agent-runtime-py 0.1.1__tar.gz → 0.2.0__tar.gz
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.
- agent_runtime_py-0.2.0/CHANGELOG.md +8 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/PKG-INFO +6 -5
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/README.md +2 -2
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/pyproject.toml +6 -6
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/claude.py +3 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/hooks.py +10 -25
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/monitor_worker.py +19 -7
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/sessions.py +12 -1
- agent_runtime_py-0.2.0/tests/test_hooks.py +179 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/tests/test_monitors.py +40 -5
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/uv.lock +14 -80
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/.github/workflows/ci.yml +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/.github/workflows/release.yml +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/.gitignore +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/LICENSE +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/__init__.py +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/bridge.py +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/monitors.py +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/operations.py +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/store.py +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/src/agent_runtime/worker.py +0 -0
- {agent_runtime_py-0.1.1 → agent_runtime_py-0.2.0}/tests/test_claude_runtime.py +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Breaking: requires typed-agent-hooks 0.3. Run the hooks with `agent_runtime.hooks.app.main()` and the host name (`codex` or `claude`) as the only argument; the host hook file sets the timeout. The `agent-runtime-hooks` console script is removed.
|
|
6
|
+
- Breaking: the Claude Agent SDK moves to the `claude` extra. `ClaudeRuntime` needs `agent-runtime-py[claude]`.
|
|
7
|
+
- Fix: a monitor worker whose command exceeded the output limit never exited and ignored SIGTERM.
|
|
8
|
+
- Fix: a Claude SessionStart, which carries no permission mode, kept the mode empty; it now keeps the mode that the same live Claude process reported.
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-runtime-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Reconnectable native agent workers with durable input receipts and events
|
|
5
5
|
Project-URL: Repository, https://github.com/nimashoghi/agent-runtime
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
|
-
Requires-Dist: claude-agent-sdk==0.2.159
|
|
10
9
|
Requires-Dist: filelock<4,>=3.16
|
|
11
10
|
Requires-Dist: platformdirs<5,>=4
|
|
12
11
|
Requires-Dist: psutil<8,>=7
|
|
13
|
-
Requires-Dist: typed-agent-hooks
|
|
12
|
+
Requires-Dist: typed-agent-hooks<0.4,>=0.3
|
|
13
|
+
Provides-Extra: claude
|
|
14
|
+
Requires-Dist: claude-agent-sdk==0.2.159; extra == 'claude'
|
|
14
15
|
Description-Content-Type: text/markdown
|
|
15
16
|
|
|
16
17
|
# Agent runtime
|
|
17
18
|
|
|
18
|
-
`agent-runtime-py` provides detached Claude Code workers that applications can reconnect to after restarting. Import it as `agent_runtime`. It uses the official Claude Agent SDK and your installed Claude Code executable, loading native user and project settings, hooks, skills, and MCP servers.
|
|
19
|
+
`agent-runtime-py` provides detached Claude Code workers that applications can reconnect to after restarting. Import it as `agent_runtime`. The workers need the `claude` extra (`agent-runtime-py[claude]`), which installs the Claude Agent SDK; the hooks, monitors and peer messages do not. It uses the official Claude Agent SDK and your installed Claude Code executable, loading native user and project settings, hooks, skills, and MCP servers.
|
|
19
20
|
|
|
20
21
|
This repository is under active development. The current implementation supports macOS and Linux and has been exercised with Claude Code 2.1.281 and Claude Agent SDK 0.2.159. Windows is not supported.
|
|
21
22
|
|
|
@@ -49,7 +50,7 @@ The default permission mode is Claude's ordinary permission behavior. Pending ap
|
|
|
49
50
|
|
|
50
51
|
## Monitors and local peer messages
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
Run the hooks from an executable PEP 723 script with an exact `agent-runtime-py` pin and a `[tool.uv] exclude-newer` date, whose body is `from agent_runtime.hooks import app` and `if __name__ == "__main__": app.main()`. Register that script for SessionStart, UserPromptSubmit and PreToolUse in each host's hook file, with the host name (`codex` or `claude`) as its only argument and a timeout of 10 seconds. End each command with `|| true`: uv exits nonzero when it cannot start the script (2, for example, when the package index is unreachable), and both hosts read exit 2 on UserPromptSubmit and PreToolUse as a block. The hooks register native process identities and current permission modes. Claude hooks start a private delivery child whose inherited native inbox token remains in memory; it is never written to the registry or used for messages to another session.
|
|
53
54
|
|
|
54
55
|
`agent_runtime.operations` supplies `monitor_start`, `monitor_list`, `monitor_get`, `monitor_stop`, `monitor_events`, `agents`, and `send`. Codex operations delegate to the installed `codexr` CLI. Claude monitors run a detached foreground Bash command and retain stdout observations in SQLite; stderr goes to a private log. A 1 MiB output limit stops an overly verbose monitor. Delivery batches retain each monitor's launch permission class. Native Claude receives these as peer context and applies its own admission policy, including holding messages if permission modes require approval. `transport-written` confirms only a socket write, not admission or model processing. Interrupted writes remain uncertain and are not replayed automatically.
|
|
55
56
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent runtime
|
|
2
2
|
|
|
3
|
-
`agent-runtime-py` provides detached Claude Code workers that applications can reconnect to after restarting. Import it as `agent_runtime`. It uses the official Claude Agent SDK and your installed Claude Code executable, loading native user and project settings, hooks, skills, and MCP servers.
|
|
3
|
+
`agent-runtime-py` provides detached Claude Code workers that applications can reconnect to after restarting. Import it as `agent_runtime`. The workers need the `claude` extra (`agent-runtime-py[claude]`), which installs the Claude Agent SDK; the hooks, monitors and peer messages do not. It uses the official Claude Agent SDK and your installed Claude Code executable, loading native user and project settings, hooks, skills, and MCP servers.
|
|
4
4
|
|
|
5
5
|
This repository is under active development. The current implementation supports macOS and Linux and has been exercised with Claude Code 2.1.281 and Claude Agent SDK 0.2.159. Windows is not supported.
|
|
6
6
|
|
|
@@ -34,7 +34,7 @@ The default permission mode is Claude's ordinary permission behavior. Pending ap
|
|
|
34
34
|
|
|
35
35
|
## Monitors and local peer messages
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Run the hooks from an executable PEP 723 script with an exact `agent-runtime-py` pin and a `[tool.uv] exclude-newer` date, whose body is `from agent_runtime.hooks import app` and `if __name__ == "__main__": app.main()`. Register that script for SessionStart, UserPromptSubmit and PreToolUse in each host's hook file, with the host name (`codex` or `claude`) as its only argument and a timeout of 10 seconds. End each command with `|| true`: uv exits nonzero when it cannot start the script (2, for example, when the package index is unreachable), and both hosts read exit 2 on UserPromptSubmit and PreToolUse as a block. The hooks register native process identities and current permission modes. Claude hooks start a private delivery child whose inherited native inbox token remains in memory; it is never written to the registry or used for messages to another session.
|
|
38
38
|
|
|
39
39
|
`agent_runtime.operations` supplies `monitor_start`, `monitor_list`, `monitor_get`, `monitor_stop`, `monitor_events`, `agents`, and `send`. Codex operations delegate to the installed `codexr` CLI. Claude monitors run a detached foreground Bash command and retain stdout observations in SQLite; stderr goes to a private log. A 1 MiB output limit stops an overly verbose monitor. Delivery batches retain each monitor's launch permission class. Native Claude receives these as peer context and applies its own admission policy, including holding messages if permission modes require approval. `transport-written` confirms only a socket write, not admission or model processing. Interrupted writes remain uncertain and are not replayed automatically.
|
|
40
40
|
|
|
@@ -4,22 +4,22 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-runtime-py"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Reconnectable native agent workers with durable input receipts and events"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
12
|
requires-python = ">=3.11"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"claude-agent-sdk==0.2.159",
|
|
15
14
|
"filelock>=3.16,<4",
|
|
16
15
|
"platformdirs>=4,<5",
|
|
17
16
|
"psutil>=7,<8",
|
|
18
|
-
"typed-agent-hooks",
|
|
17
|
+
"typed-agent-hooks>=0.3,<0.4",
|
|
19
18
|
]
|
|
20
19
|
|
|
21
|
-
[project.
|
|
22
|
-
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
# Only the detached Claude worker (`ClaudeRuntime`) uses the SDK; hooks and monitors do not.
|
|
22
|
+
claude = ["claude-agent-sdk==0.2.159"]
|
|
23
23
|
|
|
24
24
|
[project.urls]
|
|
25
25
|
Repository = "https://github.com/nimashoghi/agent-runtime"
|
|
@@ -28,7 +28,7 @@ Repository = "https://github.com/nimashoghi/agent-runtime"
|
|
|
28
28
|
packages = ["src/agent_runtime"]
|
|
29
29
|
|
|
30
30
|
[dependency-groups]
|
|
31
|
-
dev = ["pytest>=8.4,<9", "pytest-asyncio>=1.1,<2", "ruff>=0.12,<1", "basedpyright>=1.31,<2", "build>=1.3,<2"]
|
|
31
|
+
dev = ["agent-runtime-py[claude]", "pytest>=8.4,<9", "pytest-asyncio>=1.1,<2", "ruff>=0.12,<1", "basedpyright>=1.31,<2", "build>=1.3,<2"]
|
|
32
32
|
|
|
33
33
|
[tool.pytest.ini_options]
|
|
34
34
|
asyncio_mode = "auto"
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import hashlib
|
|
7
|
+
import importlib.util
|
|
7
8
|
import json
|
|
8
9
|
import math
|
|
9
10
|
import os
|
|
@@ -77,6 +78,8 @@ class ClaudeRuntime:
|
|
|
77
78
|
"""The client owns no worker lifetime and can reconnect after a service restart."""
|
|
78
79
|
|
|
79
80
|
def __init__(self, directory: Path) -> None:
|
|
81
|
+
if importlib.util.find_spec("claude_agent_sdk") is None:
|
|
82
|
+
raise RuntimeError("ClaudeRuntime needs the claude extra: agent-runtime-py[claude]")
|
|
80
83
|
self.store = Store(directory)
|
|
81
84
|
|
|
82
85
|
def inspect(self, execution_id: str) -> dict[str, Any]:
|
|
@@ -14,15 +14,18 @@ from agent_runtime.sessions import Registry, own_claude_registration
|
|
|
14
14
|
app = shared.HookApp(name="agent-runtime")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def register(
|
|
17
|
+
def register(
|
|
18
|
+
event: shared.events.SessionStarted
|
|
19
|
+
| shared.events.PromptSubmitted
|
|
20
|
+
| shared.events.ToolCallProposed,
|
|
21
|
+
) -> None:
|
|
18
22
|
context = event.context
|
|
19
23
|
if context.agent_id:
|
|
20
24
|
return
|
|
21
25
|
registry = Registry()
|
|
22
|
-
|
|
23
|
-
if provider == "codex":
|
|
26
|
+
if context.host == "codex":
|
|
24
27
|
registry.update(
|
|
25
|
-
|
|
28
|
+
"codex", context.session_id, cwd=context.cwd, permission_mode=context.permission_mode
|
|
26
29
|
)
|
|
27
30
|
return
|
|
28
31
|
with FileLock(str(registry.store.root / f"bridge-start-{context.session_id}.lock"), timeout=5):
|
|
@@ -59,24 +62,6 @@ def register(event) -> None:
|
|
|
59
62
|
)
|
|
60
63
|
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@app.on(shared.events.PromptSubmitted, timeout=10)
|
|
68
|
-
def prompt_submitted(event: shared.events.PromptSubmitted) -> None:
|
|
69
|
-
register(event)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@app.on(shared.events.ToolCallProposed, timeout=10)
|
|
73
|
-
def tool_call_proposed(event: shared.events.ToolCallProposed) -> None:
|
|
74
|
-
register(event)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def main() -> None:
|
|
78
|
-
app.main()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if __name__ == "__main__":
|
|
82
|
-
main()
|
|
65
|
+
app.on(shared.events.SessionStarted)(register)
|
|
66
|
+
app.on(shared.events.PromptSubmitted)(register)
|
|
67
|
+
app.on(shared.events.ToolCallProposed)(register)
|
|
@@ -14,6 +14,18 @@ from agent_runtime.claude import process_alive
|
|
|
14
14
|
from agent_runtime.monitors import Monitors
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
async def exited(child: asyncio.subprocess.Process, timeout: float) -> bool:
|
|
18
|
+
"""Wait until the child exits, but not for its pipes to close.
|
|
19
|
+
|
|
20
|
+
`Process.wait()` called before the child exits returns only after stdout
|
|
21
|
+
and stderr close, and a pipe that no reader drains never closes.
|
|
22
|
+
"""
|
|
23
|
+
deadline = time.monotonic() + timeout
|
|
24
|
+
while child.returncode is None and time.monotonic() < deadline:
|
|
25
|
+
await asyncio.sleep(0.05)
|
|
26
|
+
return child.returncode is not None
|
|
27
|
+
|
|
28
|
+
|
|
17
29
|
async def run(directory: Path, identity: str) -> None:
|
|
18
30
|
monitors = Monitors(directory)
|
|
19
31
|
value = monitors.get(identity)
|
|
@@ -93,17 +105,17 @@ async def run(directory: Path, identity: str) -> None:
|
|
|
93
105
|
if stop.is_set():
|
|
94
106
|
if process_alive({"pid": child.pid, "process_created_at": child_created}):
|
|
95
107
|
os.killpg(child.pid, signal.SIGTERM)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
os.killpg(child.pid, signal.SIGKILL)
|
|
108
|
+
if not await exited(child, 5) and process_alive(
|
|
109
|
+
{"pid": child.pid, "process_created_at": child_created}
|
|
110
|
+
):
|
|
111
|
+
os.killpg(child.pid, signal.SIGKILL)
|
|
101
112
|
break
|
|
102
113
|
for reader in readers:
|
|
103
114
|
if reader.done() and (error := reader.exception()) is not None:
|
|
104
115
|
raise error
|
|
105
116
|
await asyncio.sleep(0.1)
|
|
106
|
-
|
|
117
|
+
# Stop can win the race with a normal exit; record the exit code then too.
|
|
118
|
+
await exited(child, 5)
|
|
107
119
|
try:
|
|
108
120
|
await asyncio.wait_for(asyncio.gather(*readers), timeout=5)
|
|
109
121
|
except TimeoutError as error:
|
|
@@ -134,7 +146,7 @@ async def run(directory: Path, identity: str) -> None:
|
|
|
134
146
|
if child is not None and child.returncode is None:
|
|
135
147
|
if process_alive({"pid": child.pid, "process_created_at": child_created}):
|
|
136
148
|
os.killpg(child.pid, signal.SIGKILL)
|
|
137
|
-
await child
|
|
149
|
+
await exited(child, 5)
|
|
138
150
|
for reader in readers:
|
|
139
151
|
reader.cancel()
|
|
140
152
|
await asyncio.gather(*readers, return_exceptions=True)
|
|
@@ -114,7 +114,18 @@ def own_claude_registration(
|
|
|
114
114
|
process = psutil.Process(pid)
|
|
115
115
|
if process.uids().real != os.getuid():
|
|
116
116
|
raise RuntimeError("native Claude process belongs to a different user")
|
|
117
|
-
|
|
117
|
+
registry = registry or Registry()
|
|
118
|
+
# Claude's SessionStart payload has no permission mode. Keep the mode that
|
|
119
|
+
# this same live process reported; a resumed session in a new process has
|
|
120
|
+
# none until a prompt or tool call reports one.
|
|
121
|
+
if permission_mode is None:
|
|
122
|
+
try:
|
|
123
|
+
previous = registry.get("claude_code", session_id)
|
|
124
|
+
except KeyError:
|
|
125
|
+
previous = {}
|
|
126
|
+
if previous.get("pid") == pid and process_alive(previous):
|
|
127
|
+
permission_mode = previous.get("permission_mode")
|
|
128
|
+
return registry.update(
|
|
118
129
|
"claude_code",
|
|
119
130
|
session_id,
|
|
120
131
|
cwd=cwd,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""The hooks against hand-written payloads in the native Codex and Claude shapes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from agent_runtime.claude import process_alive
|
|
15
|
+
from agent_runtime.hooks import app
|
|
16
|
+
from agent_runtime.sessions import Registry, own_claude_registration
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def wait_for(predicate, timeout=15):
|
|
20
|
+
deadline = time.monotonic() + timeout
|
|
21
|
+
while time.monotonic() < deadline:
|
|
22
|
+
if result := predicate():
|
|
23
|
+
return result
|
|
24
|
+
time.sleep(0.05)
|
|
25
|
+
raise AssertionError("condition did not become true")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def codex_payload(event: str, session_id: str, **fields) -> dict:
|
|
29
|
+
payload = {
|
|
30
|
+
"session_id": session_id,
|
|
31
|
+
"transcript_path": None,
|
|
32
|
+
"cwd": "/repo",
|
|
33
|
+
"hook_event_name": event,
|
|
34
|
+
"model": "gpt-5",
|
|
35
|
+
"permission_mode": "default",
|
|
36
|
+
}
|
|
37
|
+
if event == "SessionStart":
|
|
38
|
+
payload["source"] = "startup"
|
|
39
|
+
elif event == "UserPromptSubmit":
|
|
40
|
+
payload.update(turn_id="t", prompt="hello")
|
|
41
|
+
elif event == "PreToolUse":
|
|
42
|
+
payload.update(
|
|
43
|
+
turn_id="t", tool_name="Bash", tool_use_id="u", tool_input={"command": "echo ok"}
|
|
44
|
+
)
|
|
45
|
+
return {**payload, **fields}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.parametrize("event", ["SessionStart", "UserPromptSubmit", "PreToolUse"])
|
|
49
|
+
def test_codex_events_register_the_session(tmp_path, monkeypatch, event):
|
|
50
|
+
monkeypatch.setenv("AGENT_RUNTIME_DIRECTORY", str(tmp_path))
|
|
51
|
+
session_id = str(uuid.uuid4())
|
|
52
|
+
|
|
53
|
+
assert app.handle("codex", codex_payload(event, session_id)) is None
|
|
54
|
+
|
|
55
|
+
record = Registry(tmp_path).get("codex", session_id)
|
|
56
|
+
assert record["cwd"] == "/repo"
|
|
57
|
+
assert record["permission_mode"] == "default"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.parametrize("host", ["codex", "claude"])
|
|
61
|
+
def test_subagent_events_do_not_register(tmp_path, monkeypatch, host):
|
|
62
|
+
monkeypatch.setenv("AGENT_RUNTIME_DIRECTORY", str(tmp_path))
|
|
63
|
+
monkeypatch.delenv("CLAUDE_PID", raising=False)
|
|
64
|
+
payload = codex_payload("PreToolUse", "parent", agent_id="a", agent_type="explore")
|
|
65
|
+
if host == "claude":
|
|
66
|
+
payload["transcript_path"] = str(tmp_path / "t.jsonl")
|
|
67
|
+
|
|
68
|
+
assert app.handle(host, payload) is None
|
|
69
|
+
|
|
70
|
+
for provider in ("codex", "claude_code"):
|
|
71
|
+
with pytest.raises(KeyError):
|
|
72
|
+
Registry(tmp_path).get(provider, "parent")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_hooks_do_not_import_the_claude_sdk():
|
|
76
|
+
# Hook environments install agent-runtime-py without the `claude` extra.
|
|
77
|
+
code = "import sys, agent_runtime.hooks; print('claude_agent_sdk' in sys.modules)"
|
|
78
|
+
result = subprocess.run(
|
|
79
|
+
[sys.executable, "-c", code], capture_output=True, text=True, check=True
|
|
80
|
+
)
|
|
81
|
+
assert result.stdout == "False\n"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_hook_command_reads_host_argument_and_stdin(tmp_path):
|
|
85
|
+
session_id = str(uuid.uuid4())
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
[
|
|
88
|
+
sys.executable,
|
|
89
|
+
"-c",
|
|
90
|
+
"from agent_runtime.hooks import app\nif __name__ == '__main__':\n app.main()",
|
|
91
|
+
"codex",
|
|
92
|
+
],
|
|
93
|
+
input=json.dumps(codex_payload("SessionStart", session_id)),
|
|
94
|
+
env={"AGENT_RUNTIME_DIRECTORY": str(tmp_path), "PATH": "/usr/bin:/bin"},
|
|
95
|
+
capture_output=True,
|
|
96
|
+
text=True,
|
|
97
|
+
check=True,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert (result.stdout, result.stderr) == ("", "")
|
|
101
|
+
assert Registry(tmp_path).get("codex", session_id)["cwd"] == "/repo"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_claude_session_start_starts_one_delivery_bridge(tmp_path, monkeypatch):
|
|
105
|
+
session_id = str(uuid.uuid4())
|
|
106
|
+
payload = {
|
|
107
|
+
"session_id": session_id,
|
|
108
|
+
"transcript_path": str(tmp_path / "t.jsonl"),
|
|
109
|
+
"cwd": "/repo",
|
|
110
|
+
"hook_event_name": "SessionStart",
|
|
111
|
+
"source": "startup",
|
|
112
|
+
"model": "claude",
|
|
113
|
+
}
|
|
114
|
+
owner = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(300)"])
|
|
115
|
+
try:
|
|
116
|
+
with (
|
|
117
|
+
tempfile.TemporaryDirectory(prefix="ar-hook-", dir="/tmp") as temporary,
|
|
118
|
+
socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as inbox,
|
|
119
|
+
):
|
|
120
|
+
inbox.bind(str(Path(temporary) / "inbox.sock"))
|
|
121
|
+
inbox.listen()
|
|
122
|
+
for name, value in {
|
|
123
|
+
"AGENT_RUNTIME_DIRECTORY": str(tmp_path),
|
|
124
|
+
"CLAUDE_CODE_SESSION_ID": session_id,
|
|
125
|
+
"CLAUDE_PID": str(owner.pid),
|
|
126
|
+
"CLAUDE_CODE_MESSAGING_SOCKET": str(Path(temporary) / "inbox.sock"),
|
|
127
|
+
"CLAUDE_CODE_MESSAGING_TOKEN": "fixture-token",
|
|
128
|
+
}.items():
|
|
129
|
+
monkeypatch.setenv(name, value)
|
|
130
|
+
|
|
131
|
+
assert app.handle("claude", payload) is None
|
|
132
|
+
registry = Registry(tmp_path)
|
|
133
|
+
# The hook records the bridge before it returns.
|
|
134
|
+
bridge = registry.get("claude_code", session_id)["bridge"]
|
|
135
|
+
assert bridge["native_pid"] == owner.pid
|
|
136
|
+
assert process_alive(bridge)
|
|
137
|
+
|
|
138
|
+
prompt = {
|
|
139
|
+
**payload,
|
|
140
|
+
"hook_event_name": "UserPromptSubmit",
|
|
141
|
+
"prompt": "hi",
|
|
142
|
+
"permission_mode": "default",
|
|
143
|
+
}
|
|
144
|
+
assert app.handle("claude", prompt) is None
|
|
145
|
+
assert registry.get("claude_code", session_id)["bridge"] == bridge
|
|
146
|
+
|
|
147
|
+
# SessionStart after compaction has no permission mode.
|
|
148
|
+
assert app.handle("claude", {**payload, "source": "compact"}) is None
|
|
149
|
+
assert registry.get("claude_code", session_id)["permission_mode"] == "default"
|
|
150
|
+
finally:
|
|
151
|
+
owner.terminate()
|
|
152
|
+
owner.wait(timeout=5)
|
|
153
|
+
# The bridge exits when its native owner exits.
|
|
154
|
+
wait_for(lambda: not process_alive(bridge))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_permission_mode_belongs_to_the_native_process(tmp_path, monkeypatch):
|
|
158
|
+
registry = Registry(tmp_path)
|
|
159
|
+
monkeypatch.setenv("CLAUDE_CODE_MESSAGING_SOCKET", str(tmp_path / "inbox.sock"))
|
|
160
|
+
monkeypatch.delenv("CLAUDE_CODE_SESSION_ID", raising=False)
|
|
161
|
+
sleeper = [sys.executable, "-c", "import time; time.sleep(300)"]
|
|
162
|
+
first = subprocess.Popen(sleeper)
|
|
163
|
+
try:
|
|
164
|
+
monkeypatch.setenv("CLAUDE_PID", str(first.pid))
|
|
165
|
+
own_claude_registration("s", "/repo", "default", registry=registry)
|
|
166
|
+
# SessionStart after compaction: same process, no mode in the payload.
|
|
167
|
+
own_claude_registration("s", "/repo", None, registry=registry)
|
|
168
|
+
assert registry.get("claude_code", "s")["permission_mode"] == "default"
|
|
169
|
+
finally:
|
|
170
|
+
first.terminate()
|
|
171
|
+
first.wait(timeout=5)
|
|
172
|
+
resumed = subprocess.Popen(sleeper)
|
|
173
|
+
try:
|
|
174
|
+
monkeypatch.setenv("CLAUDE_PID", str(resumed.pid))
|
|
175
|
+
own_claude_registration("s", "/repo", None, registry=registry)
|
|
176
|
+
assert registry.get("claude_code", "s")["permission_mode"] is None
|
|
177
|
+
finally:
|
|
178
|
+
resumed.terminate()
|
|
179
|
+
resumed.wait(timeout=5)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Real detached shell monitors and native inbox frames at a local socket boundary."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
4
6
|
import socket
|
|
5
7
|
import subprocess
|
|
6
8
|
import sys
|
|
@@ -8,6 +10,7 @@ import tempfile
|
|
|
8
10
|
import threading
|
|
9
11
|
import time
|
|
10
12
|
import uuid
|
|
13
|
+
from contextlib import suppress
|
|
11
14
|
from pathlib import Path
|
|
12
15
|
|
|
13
16
|
import psutil
|
|
@@ -50,7 +53,7 @@ def inbox(tmp_path, monkeypatch):
|
|
|
50
53
|
data += block
|
|
51
54
|
frames.append([json.loads(line) for line in data.splitlines()])
|
|
52
55
|
|
|
53
|
-
thread = threading.Thread(target=receive)
|
|
56
|
+
thread = threading.Thread(target=receive, daemon=True)
|
|
54
57
|
thread.start()
|
|
55
58
|
owner = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(300)"])
|
|
56
59
|
sid = str(uuid.uuid4())
|
|
@@ -75,9 +78,18 @@ def inbox(tmp_path, monkeypatch):
|
|
|
75
78
|
finally:
|
|
76
79
|
for monitor in monitors.list(sid):
|
|
77
80
|
monitors.stop(monitor["monitor_id"])
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
deadline = time.monotonic() + 15
|
|
82
|
+
while time.monotonic() < deadline and any(map(process_alive, monitors.list(sid))):
|
|
83
|
+
time.sleep(0.05)
|
|
84
|
+
# Kill a stuck worker and its command, so that a failing test leaks
|
|
85
|
+
# no process, and fail after the rest of the cleanup.
|
|
86
|
+
stuck = [m for m in monitors.list(sid) if process_alive(m)]
|
|
87
|
+
for monitor in stuck:
|
|
88
|
+
with suppress(psutil.NoSuchProcess):
|
|
89
|
+
worker = psutil.Process(monitor["pid"])
|
|
90
|
+
for process in [*worker.children(recursive=True), worker]:
|
|
91
|
+
with suppress(psutil.NoSuchProcess):
|
|
92
|
+
process.kill()
|
|
81
93
|
if owner.poll() is None:
|
|
82
94
|
owner.terminate()
|
|
83
95
|
owner.wait(timeout=5)
|
|
@@ -85,6 +97,7 @@ def inbox(tmp_path, monkeypatch):
|
|
|
85
97
|
stopped.set()
|
|
86
98
|
thread.join(timeout=2)
|
|
87
99
|
sock.close()
|
|
100
|
+
assert not stuck, f"monitor workers did not exit: {[m['monitor_id'] for m in stuck]}"
|
|
88
101
|
|
|
89
102
|
|
|
90
103
|
def test_shell_output_failure_and_truthful_peer_mode(inbox):
|
|
@@ -121,7 +134,7 @@ def test_deadline_and_stop_kill_foreground_process_group(inbox, tmp_path):
|
|
|
121
134
|
monitor = monitors.start(
|
|
122
135
|
f"sleep 30 & echo $! > {pidfile}; wait", description="stop tree", persistent=True
|
|
123
136
|
)
|
|
124
|
-
wait_for(pidfile.exists)
|
|
137
|
+
wait_for(lambda: pidfile.exists() and pidfile.read_text().strip())
|
|
125
138
|
child = psutil.Process(int(pidfile.read_text()))
|
|
126
139
|
identity = {"pid": child.pid, "process_created_at": child.create_time()}
|
|
127
140
|
monitors.stop(monitor["monitor_id"])
|
|
@@ -190,3 +203,25 @@ def test_excessive_output_stops_with_inspectable_error(inbox):
|
|
|
190
203
|
lambda: m if (m := monitors.get(monitor["monitor_id"]))["status"] == "failed" else None
|
|
191
204
|
)
|
|
192
205
|
assert "exceeded 1 MiB" in done["error"]
|
|
206
|
+
# The unread output must not keep the worker waiting for its command.
|
|
207
|
+
wait_for(lambda: not process_alive(done))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_worker_stops_its_command_and_exits_on_sigterm(inbox, tmp_path):
|
|
211
|
+
monitors, _sid, _, _, _ = inbox
|
|
212
|
+
pidfile = tmp_path / "command.pid"
|
|
213
|
+
monitor = monitors.start(
|
|
214
|
+
f"echo $$ > {pidfile}; exec sleep 300", description="sigterm", persistent=True
|
|
215
|
+
)
|
|
216
|
+
wait_for(lambda: pidfile.exists() and pidfile.read_text().strip())
|
|
217
|
+
command = psutil.Process(int(pidfile.read_text()))
|
|
218
|
+
command_identity = {"pid": command.pid, "process_created_at": command.create_time()}
|
|
219
|
+
worker = wait_for(
|
|
220
|
+
lambda: m if (m := monitors.get(monitor["monitor_id"]))["status"] == "running" else None
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
os.kill(worker["pid"], signal.SIGTERM)
|
|
224
|
+
|
|
225
|
+
wait_for(lambda: not process_alive(worker))
|
|
226
|
+
assert not process_alive(command_identity)
|
|
227
|
+
assert monitors.get(monitor["monitor_id"])["status"] == "stopped"
|
|
@@ -9,18 +9,23 @@ resolution-markers = [
|
|
|
9
9
|
|
|
10
10
|
[[package]]
|
|
11
11
|
name = "agent-runtime-py"
|
|
12
|
-
version = "0.
|
|
12
|
+
version = "0.2.0"
|
|
13
13
|
source = { editable = "." }
|
|
14
14
|
dependencies = [
|
|
15
|
-
{ name = "claude-agent-sdk" },
|
|
16
15
|
{ name = "filelock" },
|
|
17
16
|
{ name = "platformdirs" },
|
|
18
17
|
{ name = "psutil" },
|
|
19
18
|
{ name = "typed-agent-hooks" },
|
|
20
19
|
]
|
|
21
20
|
|
|
21
|
+
[package.optional-dependencies]
|
|
22
|
+
claude = [
|
|
23
|
+
{ name = "claude-agent-sdk" },
|
|
24
|
+
]
|
|
25
|
+
|
|
22
26
|
[package.dev-dependencies]
|
|
23
27
|
dev = [
|
|
28
|
+
{ name = "agent-runtime-py", extra = ["claude"] },
|
|
24
29
|
{ name = "basedpyright" },
|
|
25
30
|
{ name = "build" },
|
|
26
31
|
{ name = "pytest" },
|
|
@@ -30,15 +35,17 @@ dev = [
|
|
|
30
35
|
|
|
31
36
|
[package.metadata]
|
|
32
37
|
requires-dist = [
|
|
33
|
-
{ name = "claude-agent-sdk", specifier = "==0.2.159" },
|
|
38
|
+
{ name = "claude-agent-sdk", marker = "extra == 'claude'", specifier = "==0.2.159" },
|
|
34
39
|
{ name = "filelock", specifier = ">=3.16,<4" },
|
|
35
40
|
{ name = "platformdirs", specifier = ">=4,<5" },
|
|
36
41
|
{ name = "psutil", specifier = ">=7,<8" },
|
|
37
|
-
{ name = "typed-agent-hooks" },
|
|
42
|
+
{ name = "typed-agent-hooks", specifier = ">=0.3,<0.4" },
|
|
38
43
|
]
|
|
44
|
+
provides-extras = ["claude"]
|
|
39
45
|
|
|
40
46
|
[package.metadata.requires-dev]
|
|
41
47
|
dev = [
|
|
48
|
+
{ name = "agent-runtime-py", extras = ["claude"] },
|
|
42
49
|
{ name = "basedpyright", specifier = ">=1.31,<2" },
|
|
43
50
|
{ name = "build", specifier = ">=1.3,<2" },
|
|
44
51
|
{ name = "pytest", specifier = ">=8.4,<9" },
|
|
@@ -294,30 +301,6 @@ wheels = [
|
|
|
294
301
|
{ url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" },
|
|
295
302
|
]
|
|
296
303
|
|
|
297
|
-
[[package]]
|
|
298
|
-
name = "cyclopts"
|
|
299
|
-
version = "4.25.3"
|
|
300
|
-
source = { registry = "https://pypi.org/simple" }
|
|
301
|
-
dependencies = [
|
|
302
|
-
{ name = "attrs" },
|
|
303
|
-
{ name = "docstring-parser" },
|
|
304
|
-
{ name = "rich" },
|
|
305
|
-
{ name = "rich-rst" },
|
|
306
|
-
]
|
|
307
|
-
sdist = { url = "https://files.pythonhosted.org/packages/c1/08/a444f3cca86cb27bc256831fc8c42b578390f4087bf6631cccb5334bd9ed/cyclopts-4.25.3.tar.gz", hash = "sha256:7d3a067bc7699a1a65fe2ea0b092a925855bb711a05adc1e65c5b9ba307acea4", size = 202814, upload-time = "2026-09-17T16:25:16.892Z" }
|
|
308
|
-
wheels = [
|
|
309
|
-
{ url = "https://files.pythonhosted.org/packages/a9/cc/e879da3d54cf727f90e6d90118e132672c3913338b566740486f8797190d/cyclopts-4.25.3-py3-none-any.whl", hash = "sha256:40e76b3153ea90e885abd8d3a4d8184db5a5eec2fde8540025aae8d72fc973fc", size = 242748, upload-time = "2026-09-17T16:25:15.509Z" },
|
|
310
|
-
]
|
|
311
|
-
|
|
312
|
-
[[package]]
|
|
313
|
-
name = "docstring-parser"
|
|
314
|
-
version = "0.18.0"
|
|
315
|
-
source = { registry = "https://pypi.org/simple" }
|
|
316
|
-
sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
|
|
317
|
-
wheels = [
|
|
318
|
-
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
|
|
319
|
-
]
|
|
320
|
-
|
|
321
304
|
[[package]]
|
|
322
305
|
name = "filelock"
|
|
323
306
|
version = "3.32.7"
|
|
@@ -420,18 +403,6 @@ wheels = [
|
|
|
420
403
|
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
|
421
404
|
]
|
|
422
405
|
|
|
423
|
-
[[package]]
|
|
424
|
-
name = "markdown-it-py"
|
|
425
|
-
version = "4.2.0"
|
|
426
|
-
source = { registry = "https://pypi.org/simple" }
|
|
427
|
-
dependencies = [
|
|
428
|
-
{ name = "mdurl" },
|
|
429
|
-
]
|
|
430
|
-
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
|
431
|
-
wheels = [
|
|
432
|
-
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
|
433
|
-
]
|
|
434
|
-
|
|
435
406
|
[[package]]
|
|
436
407
|
name = "mcp"
|
|
437
408
|
version = "2.2.0"
|
|
@@ -470,15 +441,6 @@ wheels = [
|
|
|
470
441
|
{ url = "https://files.pythonhosted.org/packages/8f/d7/6ffba5d8cd5dd9b8a19478875c50e04945314ba5074e84d749283f27f62d/mcp_types-2.2.0-py3-none-any.whl", hash = "sha256:ea476b73ee86709ab5abc9452385ed36cc05907e582355622e294595c9a04f13", size = 69106, upload-time = "2026-09-07T16:06:21.461Z" },
|
|
471
442
|
]
|
|
472
443
|
|
|
473
|
-
[[package]]
|
|
474
|
-
name = "mdurl"
|
|
475
|
-
version = "0.1.2"
|
|
476
|
-
source = { registry = "https://pypi.org/simple" }
|
|
477
|
-
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
478
|
-
wheels = [
|
|
479
|
-
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
480
|
-
]
|
|
481
|
-
|
|
482
444
|
[[package]]
|
|
483
445
|
name = "nodejs-wheel-binaries"
|
|
484
446
|
version = "24.19.0"
|
|
@@ -794,32 +756,6 @@ wheels = [
|
|
|
794
756
|
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
|
|
795
757
|
]
|
|
796
758
|
|
|
797
|
-
[[package]]
|
|
798
|
-
name = "rich"
|
|
799
|
-
version = "15.0.0"
|
|
800
|
-
source = { registry = "https://pypi.org/simple" }
|
|
801
|
-
dependencies = [
|
|
802
|
-
{ name = "markdown-it-py" },
|
|
803
|
-
{ name = "pygments" },
|
|
804
|
-
]
|
|
805
|
-
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
|
806
|
-
wheels = [
|
|
807
|
-
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
|
808
|
-
]
|
|
809
|
-
|
|
810
|
-
[[package]]
|
|
811
|
-
name = "rich-rst"
|
|
812
|
-
version = "2.1.0"
|
|
813
|
-
source = { registry = "https://pypi.org/simple" }
|
|
814
|
-
dependencies = [
|
|
815
|
-
{ name = "pygments" },
|
|
816
|
-
{ name = "rich" },
|
|
817
|
-
]
|
|
818
|
-
sdist = { url = "https://files.pythonhosted.org/packages/e2/d6/d0b9fafc73b65767200da027acab1db1bdb1048f4fea5ebf659df01c700e/rich_rst-2.1.0.tar.gz", hash = "sha256:f4d117b49697f338769759fa5cacf5197da4888b347b9fda2e50aef5cd8d93bd", size = 302732, upload-time = "2026-07-05T02:59:44.308Z" }
|
|
819
|
-
wheels = [
|
|
820
|
-
{ url = "https://files.pythonhosted.org/packages/2a/68/1fc93dd759605b5d00fc98b50200739e41ed32bd22d6ba35ca6c3932371b/rich_rst-2.1.0-py3-none-any.whl", hash = "sha256:7ecd1343ee12c879d0e7ae74c3eb6d263b023d2929c6d114212eb1fd91057255", size = 272987, upload-time = "2026-07-05T02:59:42.792Z" },
|
|
821
|
-
]
|
|
822
|
-
|
|
823
759
|
[[package]]
|
|
824
760
|
name = "rpds-py"
|
|
825
761
|
version = "2026.6.3"
|
|
@@ -1014,16 +950,14 @@ wheels = [
|
|
|
1014
950
|
|
|
1015
951
|
[[package]]
|
|
1016
952
|
name = "typed-agent-hooks"
|
|
1017
|
-
version = "0.
|
|
953
|
+
version = "0.3.0"
|
|
1018
954
|
source = { registry = "https://pypi.org/simple" }
|
|
1019
955
|
dependencies = [
|
|
1020
|
-
{ name = "cyclopts" },
|
|
1021
956
|
{ name = "pydantic" },
|
|
1022
|
-
{ name = "pydantic-core" },
|
|
1023
957
|
]
|
|
1024
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
958
|
+
sdist = { url = "https://files.pythonhosted.org/packages/cc/0e/770d2d2cca62894fe170fc266f9b1fefbfde0e6ac03a8873a1ea5f6e33b4/typed_agent_hooks-0.3.0.tar.gz", hash = "sha256:344dd64d87b8b76044b96dae9fa4f0ad2430f1ed1597520b7394058a85e83a55", size = 16284, upload-time = "2026-10-27T20:32:39.757Z" }
|
|
1025
959
|
wheels = [
|
|
1026
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
960
|
+
{ url = "https://files.pythonhosted.org/packages/de/45/b374fadf78a07adaf99649c3d054d43495dc0038593c3aafcf382397bc5f/typed_agent_hooks-0.3.0-py3-none-any.whl", hash = "sha256:b41020aafc2c913da7f362dfd7570da1fc6b9039d403e8078ea5f5ece8ae5dd2", size = 22930, upload-time = "2026-10-27T20:32:38.47Z" },
|
|
1027
961
|
]
|
|
1028
962
|
|
|
1029
963
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|