agentshim 0.1.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.
- agentshim/__init__.py +24 -0
- agentshim/base.py +52 -0
- agentshim/claude/__init__.py +3 -0
- agentshim/claude/agent.py +264 -0
- agentshim/claude/events.py +149 -0
- agentshim/claude/hooks/__init__.py +0 -0
- agentshim/claude/hooks/confine_reads.py +77 -0
- agentshim/claude_events.py +21 -0
- agentshim/cli_agent.py +433 -0
- agentshim/codex/__init__.py +3 -0
- agentshim/codex/agent.py +236 -0
- agentshim/codex/events.py +216 -0
- agentshim/codex_events.py +23 -0
- agentshim/events.py +24 -0
- agentshim/gemini/__init__.py +3 -0
- agentshim/gemini/agent.py +234 -0
- agentshim/gemini/events.py +78 -0
- agentshim/gemini_events.py +11 -0
- agentshim/llm_client.py +64 -0
- agentshim/mcp_config.py +25 -0
- agentshim/opencode/__init__.py +3 -0
- agentshim/opencode/agent.py +196 -0
- agentshim/opencode/events.py +90 -0
- agentshim/opencode_events.py +11 -0
- agentshim/py.typed +0 -0
- agentshim/sandbox.py +133 -0
- agentshim/subagent.py +93 -0
- agentshim/trajectory.py +168 -0
- agentshim/usage.py +63 -0
- agentshim/utils.py +120 -0
- agentshim-0.1.0.dist-info/METADATA +61 -0
- agentshim-0.1.0.dist-info/RECORD +33 -0
- agentshim-0.1.0.dist-info/WHEEL +4 -0
agentshim/usage.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Token/turn usage value objects for CLI-based coding agents.
|
|
2
|
+
|
|
3
|
+
Duplicated in shape (not in import) from ``libs/pydantic_agent/_usage.py``
|
|
4
|
+
so ``agentshim`` does not depend on the pydantic-ai stack. The
|
|
5
|
+
``to_dict()`` output shape is the shared contract between the two copies
|
|
6
|
+
and is pinned by ``tests/unit/test_usage_schema_parity.py``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class TokenUsage:
|
|
17
|
+
"""Token counts for a CLI-agent session.
|
|
18
|
+
|
|
19
|
+
Invariant: ``cached_input_tokens`` is a subset of ``input_tokens``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
input_tokens: int = 0
|
|
23
|
+
output_tokens: int = 0
|
|
24
|
+
cached_input_tokens: int = 0
|
|
25
|
+
turns: int = 0
|
|
26
|
+
|
|
27
|
+
def __add__(self, other: TokenUsage) -> TokenUsage:
|
|
28
|
+
return TokenUsage(
|
|
29
|
+
input_tokens=self.input_tokens + other.input_tokens,
|
|
30
|
+
output_tokens=self.output_tokens + other.output_tokens,
|
|
31
|
+
cached_input_tokens=self.cached_input_tokens + other.cached_input_tokens,
|
|
32
|
+
turns=self.turns + other.turns,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict[str, int]:
|
|
36
|
+
return {
|
|
37
|
+
"input_tokens": self.input_tokens,
|
|
38
|
+
"output_tokens": self.output_tokens,
|
|
39
|
+
"cached_input_tokens": self.cached_input_tokens,
|
|
40
|
+
"turns": self.turns,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ProviderUsage:
|
|
46
|
+
"""Per-session usage bundle: common token counts + provider extras.
|
|
47
|
+
|
|
48
|
+
``provider`` is the registered provider name ("claude", "codex",
|
|
49
|
+
"gemini", "opencode"). ``total_cost_usd`` is ``None`` when the
|
|
50
|
+
provider does not report cost in its stream (codex, gemini, opencode
|
|
51
|
+
depending on config).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
tokens: TokenUsage = field(default_factory=TokenUsage)
|
|
55
|
+
total_cost_usd: float | None = None
|
|
56
|
+
provider: str = ""
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict[str, Any]:
|
|
59
|
+
return {
|
|
60
|
+
**self.tokens.to_dict(),
|
|
61
|
+
"total_cost_usd": self.total_cost_usd,
|
|
62
|
+
"provider": self.provider,
|
|
63
|
+
}
|
agentshim/utils.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_interactive_env() -> dict[str, str]:
|
|
11
|
+
"""Capture environment variables from an interactive shell."""
|
|
12
|
+
try:
|
|
13
|
+
# Run env in an interactive shell to get the full user environment
|
|
14
|
+
# Use start_new_session=True (setsid) to detach from TTY and avoid
|
|
15
|
+
# SIGTTOU/SIGTTIN signals when bash -i tries to set process group
|
|
16
|
+
result = subprocess.run(
|
|
17
|
+
["/bin/bash", "-i", "-c", "env"], capture_output=True, text=True, check=False, start_new_session=True
|
|
18
|
+
)
|
|
19
|
+
if result.returncode != 0:
|
|
20
|
+
return os.environ.copy()
|
|
21
|
+
|
|
22
|
+
env: dict[str, str] = {}
|
|
23
|
+
for line in result.stdout.splitlines():
|
|
24
|
+
if "=" in line:
|
|
25
|
+
key, value = line.split("=", 1)
|
|
26
|
+
env[key] = value
|
|
27
|
+
return env
|
|
28
|
+
except Exception:
|
|
29
|
+
return os.environ.copy()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# -- File generation system prompt (shared by RLMCodingAgent & SubagentCodingAgent) --
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
FILE_GEN_SYSTEM_PROMPT = (
|
|
36
|
+
"You are a deployment assistant. The user will ask you to generate "
|
|
37
|
+
"one or more files. For EACH file, output a section in this exact format:\n\n"
|
|
38
|
+
"FILE: .sds/<filename>\n"
|
|
39
|
+
"```\n"
|
|
40
|
+
"<file content here>\n"
|
|
41
|
+
"```\n\n"
|
|
42
|
+
"Output ONLY these sections. Do not add explanations outside the sections."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_and_write_files(
|
|
47
|
+
raw: str,
|
|
48
|
+
prompt: str,
|
|
49
|
+
repo_path: Path,
|
|
50
|
+
log_label: str,
|
|
51
|
+
) -> list[str]:
|
|
52
|
+
"""Parse ``FILE: .sds/<name>`` sections from *raw* LLM output and write them.
|
|
53
|
+
|
|
54
|
+
Returns a list of relative paths that were written. If no ``FILE:`` sections
|
|
55
|
+
are found but the prompt references exactly one ``.sds/`` file, the entire
|
|
56
|
+
*raw* response (with markdown fences stripped) is written as that file
|
|
57
|
+
(fallback mode).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
raw: Raw LLM response text.
|
|
61
|
+
prompt: The original prompt (used to detect expected output files).
|
|
62
|
+
repo_path: Root of the repository.
|
|
63
|
+
log_label: Label for log messages (e.g. ``"[RLM]"``, ``"[Subagent]"``).
|
|
64
|
+
"""
|
|
65
|
+
expected_files = re.findall(r"\.sds/[\w._-]+", prompt)
|
|
66
|
+
|
|
67
|
+
written: list[str] = []
|
|
68
|
+
file_sections = re.findall(
|
|
69
|
+
r"(?:^|\n)\s*(?:#+\s*)?FILE:\s*(\.sds/[\w./-]+)\s*\n```[^\n]*\n(.*?)```",
|
|
70
|
+
raw,
|
|
71
|
+
re.DOTALL | re.IGNORECASE,
|
|
72
|
+
)
|
|
73
|
+
for rel_path, content in file_sections:
|
|
74
|
+
out_path = repo_path / rel_path
|
|
75
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
out_path.write_text(content)
|
|
77
|
+
logger.info(f"{log_label} Wrote {out_path}")
|
|
78
|
+
written.append(rel_path)
|
|
79
|
+
|
|
80
|
+
if not written and len(expected_files) == 1:
|
|
81
|
+
out_path = repo_path / expected_files[0]
|
|
82
|
+
content = re.sub(r"^```[^\n]*\n|```$", "", raw.strip(), flags=re.MULTILINE)
|
|
83
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
out_path.write_text(content)
|
|
85
|
+
logger.info(f"{log_label} Wrote {out_path} (fallback)")
|
|
86
|
+
written.append(expected_files[0])
|
|
87
|
+
|
|
88
|
+
return written
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
DEFAULT_TOOL_PARAM_MAX_LEN = 200
|
|
92
|
+
MEMORY_TOOL_PARAM_MAX_LEN = 10000
|
|
93
|
+
_VERBOSE_TOOL_NAMES = frozenset({"recall_incident", "store_incident"})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def truncate_params(params: Any, max_len: int = DEFAULT_TOOL_PARAM_MAX_LEN) -> str:
|
|
97
|
+
"""Truncate a parameter representation to *max_len* characters."""
|
|
98
|
+
s = str(params)
|
|
99
|
+
if len(s) > max_len:
|
|
100
|
+
return s[:max_len] + "..."
|
|
101
|
+
return s
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def truncate_tool_params(tool_name: str, params: Any) -> str:
|
|
105
|
+
"""Render tool parameters with per-tool truncation rules.
|
|
106
|
+
|
|
107
|
+
Most tools keep the historical compact preview so CLI logs stay readable.
|
|
108
|
+
Incident-memory tools get a much larger budget because their payloads are
|
|
109
|
+
often the exact debugging context we need to inspect.
|
|
110
|
+
"""
|
|
111
|
+
max_len = MEMORY_TOOL_PARAM_MAX_LEN if tool_name in _VERBOSE_TOOL_NAMES else DEFAULT_TOOL_PARAM_MAX_LEN
|
|
112
|
+
return truncate_params(params, max_len=max_len)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def truncate_content(content: str, max_lines: int = 10) -> str:
|
|
116
|
+
"""Truncate *content* keeping the first and last *max_lines* lines."""
|
|
117
|
+
lines = content.splitlines()
|
|
118
|
+
if len(lines) > max_lines * 2:
|
|
119
|
+
return "\n".join(lines[:max_lines] + ["... (truncated) ..."] + lines[-max_lines:])
|
|
120
|
+
return content
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentshim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-agnostic coding-agent CLI shims
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: litellm>=1.0.0
|
|
7
|
+
Requires-Dist: loguru>=0.7.2
|
|
8
|
+
Provides-Extra: test
|
|
9
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# agentshim
|
|
13
|
+
|
|
14
|
+
`agentshim` is a small Python package that wraps coding-agent CLIs behind a
|
|
15
|
+
stable interface for higher-level runtimes.
|
|
16
|
+
|
|
17
|
+
It currently includes:
|
|
18
|
+
|
|
19
|
+
- a `CodingAgent` base class and provider registry
|
|
20
|
+
- CLI adapters for Claude Code, Codex, Gemini, and Opencode
|
|
21
|
+
- MCP server config dataclasses
|
|
22
|
+
- sandbox settings helpers for Claude Code
|
|
23
|
+
- a lightweight litellm client and isolated subagent helper
|
|
24
|
+
- a trajectory protocol with a no-op implementation
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install agentshim
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For development:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv sync --dev
|
|
36
|
+
uv run pytest
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Example
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from agentshim.claude import ClaudeCodeCodingAgent
|
|
43
|
+
|
|
44
|
+
agent = ClaudeCodeCodingAgent(model="sonnet")
|
|
45
|
+
result = agent.generate("Summarize the repository layout.", cwd=".")
|
|
46
|
+
print(result)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Publishing
|
|
50
|
+
|
|
51
|
+
Build locally with:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Publish with:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv publish
|
|
61
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
agentshim/__init__.py,sha256=6jsY9Z3wuEl_OZcyrLJWmSzl6tyAR24oUW99UMXbpZc,692
|
|
2
|
+
agentshim/base.py,sha256=Z23Ca3ufd9qGDBK9ER4OeCGEpgdlNgX8t1aSpWhv1UQ,1425
|
|
3
|
+
agentshim/claude_events.py,sha256=U8A1IH4dsVHxfHrq28ghAUv1m6P2H_1ZHTp6SnJ3irw,375
|
|
4
|
+
agentshim/cli_agent.py,sha256=INdN67V10KOvS9v0miImx3YjRZz6PJscw34WQ3SnaHM,15623
|
|
5
|
+
agentshim/codex_events.py,sha256=GrUlbtvE_xh00tLWpMqBOnoKjEw4R31jubKbSctjwIQ,441
|
|
6
|
+
agentshim/events.py,sha256=meWWo8MX-25ByDtYqmYCzZ93KyYMWgKYpXJcT8dT8aw,622
|
|
7
|
+
agentshim/gemini_events.py,sha256=6mlITPfx_WjX-4mDZbt5FVw4iJCcuSAnjxdeP9Rm4ow,282
|
|
8
|
+
agentshim/llm_client.py,sha256=sOXrvp6qJiFWx6VAy-szNkapli55qRjIK7_yed0fHvQ,2245
|
|
9
|
+
agentshim/mcp_config.py,sha256=q_gJY0k89a0AHPUe_9KNNIHyb1xr5SAHMEke_XMrCmc,710
|
|
10
|
+
agentshim/opencode_events.py,sha256=S54kA-3olnkqm1sHD_CKRt_KnrRzEp6Nhdu2E08ppao,294
|
|
11
|
+
agentshim/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
agentshim/sandbox.py,sha256=pNEZM85cXq3Swh9S_Vkb2V-02QzEF5RH2MKWwWaUk14,6071
|
|
13
|
+
agentshim/subagent.py,sha256=S_mfykQ9envLQ1BshbXEg4e8sTxI3Db-mQgcQ7W3hzg,3264
|
|
14
|
+
agentshim/trajectory.py,sha256=xjLWv-EXc6_U3ubHg8SATvfgTMn4AEnMxiqG4Oc79wY,5064
|
|
15
|
+
agentshim/usage.py,sha256=yf_M9PkDyDZnz9pv8Z8oISeHSRqp0N62NeemFQ1tWS0,2015
|
|
16
|
+
agentshim/utils.py,sha256=qXNOma48LaiTeEKQr8cbNejA8qtErZMXX1JldyuxUb8,4253
|
|
17
|
+
agentshim/claude/__init__.py,sha256=7-enwzQK90mpHy_Z-IKkkoErwCL8aIfalo3SV24l7f0,130
|
|
18
|
+
agentshim/claude/agent.py,sha256=BVbiW7M9cpQpSBEE9mr13BxZArrK6ki8ywNcmHl_gGE,10310
|
|
19
|
+
agentshim/claude/events.py,sha256=90O7QFre0mNKTn7BX8MaSWewg1tpywDLxdiZ7cwHVwI,5075
|
|
20
|
+
agentshim/claude/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
agentshim/claude/hooks/confine_reads.py,sha256=kWDxAi1MyPtDNnGmuD0ZNrnIkzDdkjctr2oaBwiDIPc,2237
|
|
22
|
+
agentshim/codex/__init__.py,sha256=bgHz_1JcfcEeUwJwCBqWjauiIvhun7HoJkzdhCBTPQE,118
|
|
23
|
+
agentshim/codex/agent.py,sha256=ttZEbiLja5IAjPi1vxp6_X1zzvAEa-m-pFKLjF26aR4,8500
|
|
24
|
+
agentshim/codex/events.py,sha256=I4u5Uug1ku12a44gouBNJgZGVJdCNx2o8xCQFUZi3Rg,7602
|
|
25
|
+
agentshim/gemini/__init__.py,sha256=HYgiOZwekbPGaTn6aQURuLa7ZIoUmienU_Tr4apDdsI,122
|
|
26
|
+
agentshim/gemini/agent.py,sha256=DlJn2YdlLfkRbZhyjyhURJOhzOWE0v1SWlMgqhG434o,8500
|
|
27
|
+
agentshim/gemini/events.py,sha256=Mj_poV6MzCAWRjWLD9-GJ_U3VbZGdzdEPD7jsC5IsnY,2727
|
|
28
|
+
agentshim/opencode/__init__.py,sha256=mQPn-hR4lIIxjKlrdc0byhdqlD9f0ST-UkKK_1vedHk,130
|
|
29
|
+
agentshim/opencode/agent.py,sha256=I9-vqXxgi6eUNOxgFsBsTa7jEW7_r6gfpu4kTVlV35w,7103
|
|
30
|
+
agentshim/opencode/events.py,sha256=svYi3zP9cziaUPnKlxCH3pdj41qUcvOP2FXb0qOhvks,3027
|
|
31
|
+
agentshim-0.1.0.dist-info/METADATA,sha256=FgjWfvVSOLZrUoU3OtDEj-f0vFJzxEp0RIj8JlLJYrE,1167
|
|
32
|
+
agentshim-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
33
|
+
agentshim-0.1.0.dist-info/RECORD,,
|