agentshim 0.5.0__tar.gz → 0.5.1__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.
- {agentshim-0.5.0 → agentshim-0.5.1}/.github/workflows/ci.yml +16 -1
- agentshim-0.5.1/.github/workflows/docs.yml +52 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/.gitignore +1 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/PKG-INFO +4 -1
- {agentshim-0.5.0 → agentshim-0.5.1}/README.md +3 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/base.py +32 -13
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/codex/agent.py +7 -3
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/codex/events.py +0 -9
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/copilot/agent.py +14 -10
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/copilot/events.py +2 -1
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/executor.py +7 -7
- agentshim-0.5.1/docs/api.md +3 -0
- agentshim-0.5.1/docs/development.md +27 -0
- agentshim-0.5.1/docs/events.md +137 -0
- agentshim-0.5.1/docs/executors.md +82 -0
- agentshim-0.5.1/docs/extending.md +51 -0
- agentshim-0.5.1/docs/getting-started.md +45 -0
- agentshim-0.5.1/docs/index.md +37 -0
- agentshim-0.5.1/docs/mcp.md +35 -0
- agentshim-0.5.1/docs/providers.md +23 -0
- agentshim-0.5.1/mkdocs.yml +43 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/pyproject.toml +9 -2
- agentshim-0.5.1/pyrightconfig.json +10 -0
- agentshim-0.5.1/scripts/check_errors.sh +20 -0
- agentshim-0.5.1/scripts/format_code.sh +19 -0
- agentshim-0.5.1/scripts/type_check.sh +17 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/cli_agent/test_command_executor.py +4 -6
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/llm/test_claude_stream.py +2 -1
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_event_parsing.py +2 -1
- agentshim-0.5.1/tests/unit/test_claude_usage_e2e.py +105 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_coding_agent_facade.py +10 -1
- {agentshim-0.5.0 → agentshim-0.5.1}/uv.lock +321 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/.github/workflows/publish.yml +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/__init__.py +1 -1
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/__init__.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/agent.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/events.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/hooks/__init__.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude_events.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/cli_agent.py +1 -1
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/codex/__init__.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/codex_events.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/copilot/__init__.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/copilot_events.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/events.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/gemini/__init__.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/gemini/agent.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/gemini/events.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/gemini_events.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/llm_client.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/mcp_config.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/opencode/__init__.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/opencode/agent.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/opencode/events.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/opencode_events.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/py.typed +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/sandbox.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/subagent.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/usage.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/utils.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/fixtures/copilot/session_turn_1.jsonl +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/fixtures/copilot/session_turn_2_resumed.jsonl +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/fixtures/copilot/streaming_dedup.jsonl +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/fixtures/copilot/tool_and_usage.jsonl +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/cli_agent/test_agent_cli_cleanup.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/cli_agent/test_check_cli.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/cli_agent/test_cli_prompt_passing.py +1 -1
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/llm/conftest.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/llm/test_gemini_fixture.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/llm/test_gemini_stream.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_claude.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_codex.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_copilot.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_copilot_fixtures.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_mcp_unsupported.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_resume.py +2 -2
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_sandbox.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_cli_agent_usage.py +2 -2
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_event_handlers.py +0 -0
- {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_mcp_config.py +0 -0
|
@@ -20,7 +20,22 @@ jobs:
|
|
|
20
20
|
with:
|
|
21
21
|
python-version: "3.11"
|
|
22
22
|
- run: uv sync --locked --dev
|
|
23
|
-
- run:
|
|
23
|
+
- run: bash scripts/format_code.sh --check
|
|
24
|
+
- run: bash scripts/check_errors.sh
|
|
25
|
+
|
|
26
|
+
typecheck:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: astral-sh/setup-uv@v6
|
|
31
|
+
with:
|
|
32
|
+
enable-cache: true
|
|
33
|
+
cache-dependency-glob: uv.lock
|
|
34
|
+
- uses: actions/setup-python@v5
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.11"
|
|
37
|
+
- run: uv sync --locked --dev
|
|
38
|
+
- run: bash scripts/type_check.sh
|
|
24
39
|
|
|
25
40
|
test:
|
|
26
41
|
runs-on: ubuntu-latest
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@v6
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: true
|
|
21
|
+
cache-dependency-glob: uv.lock
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.11"
|
|
26
|
+
|
|
27
|
+
- run: uv sync --locked --group docs
|
|
28
|
+
- run: uv run mkdocs build --strict --site-dir site
|
|
29
|
+
|
|
30
|
+
- name: Upload Pages artifact
|
|
31
|
+
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
|
32
|
+
uses: actions/upload-pages-artifact@v3
|
|
33
|
+
with:
|
|
34
|
+
path: site
|
|
35
|
+
|
|
36
|
+
deploy:
|
|
37
|
+
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
|
38
|
+
needs: build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
concurrency:
|
|
41
|
+
group: pages
|
|
42
|
+
cancel-in-progress: false
|
|
43
|
+
permissions:
|
|
44
|
+
pages: write
|
|
45
|
+
id-token: write
|
|
46
|
+
environment:
|
|
47
|
+
name: github-pages
|
|
48
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/configure-pages@v5
|
|
51
|
+
- id: deployment
|
|
52
|
+
uses: actions/deploy-pages@v4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentshim
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Provider-agnostic coding-agent CLI shims
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: litellm>=1.0.0
|
|
@@ -342,6 +342,9 @@ Notes:
|
|
|
342
342
|
|
|
343
343
|
```bash
|
|
344
344
|
uv sync --dev
|
|
345
|
+
bash scripts/format_code.sh --check
|
|
346
|
+
bash scripts/check_errors.sh
|
|
347
|
+
bash scripts/type_check.sh
|
|
345
348
|
uv run pytest
|
|
346
349
|
```
|
|
347
350
|
|
|
@@ -3,13 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
import inspect
|
|
4
4
|
import re
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from
|
|
7
|
-
from typing import Any, TypeVar
|
|
6
|
+
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
from agentshim.
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Callable, Sequence
|
|
10
|
+
|
|
11
|
+
from agentshim.events import AgentEventHandler
|
|
12
|
+
from agentshim.executor import CommandExecutor, CommandHandle
|
|
13
|
+
from agentshim.mcp_config import McpServerConfig
|
|
14
|
+
from agentshim.sandbox import SandboxConfig
|
|
13
15
|
|
|
14
16
|
_T = TypeVar("_T")
|
|
15
17
|
_READABLE_NAME_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
|
|
@@ -105,7 +107,7 @@ class ProviderRegistry:
|
|
|
105
107
|
self._providers: dict[str, ProviderClass] = {}
|
|
106
108
|
self._canonical_names: dict[str, str] = {}
|
|
107
109
|
|
|
108
|
-
def _normalize_name(self, name:
|
|
110
|
+
def _normalize_name(self, name: object) -> str:
|
|
109
111
|
if not isinstance(name, str):
|
|
110
112
|
raise TypeError(f"provider name must be a string, got {type(name).__name__}")
|
|
111
113
|
normalized = name.strip().lower()
|
|
@@ -122,6 +124,9 @@ class ProviderRegistry:
|
|
|
122
124
|
)
|
|
123
125
|
return canonical, normalized_aliases
|
|
124
126
|
|
|
127
|
+
def validate_names(self, canonical_name: str, aliases: tuple[str, ...]) -> None:
|
|
128
|
+
self._normalize_names(canonical_name, aliases)
|
|
129
|
+
|
|
125
130
|
def _validate_provider_class(self, cls: type[Any]) -> ProviderClass:
|
|
126
131
|
if not inspect.isclass(cls):
|
|
127
132
|
raise TypeError(f"registered provider must be a class, got {type(cls).__name__}")
|
|
@@ -200,14 +205,17 @@ def register_provider(
|
|
|
200
205
|
"""
|
|
201
206
|
|
|
202
207
|
all_aliases = (*extra_names, *aliases)
|
|
203
|
-
_PROVIDER_REGISTRY.
|
|
208
|
+
_PROVIDER_REGISTRY.validate_names(canonical_name, all_aliases)
|
|
204
209
|
|
|
205
210
|
def decorator(cls: type[_T]) -> type[_T]:
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
return cast(
|
|
212
|
+
"type[_T]",
|
|
213
|
+
_PROVIDER_REGISTRY.register(
|
|
214
|
+
cls,
|
|
215
|
+
canonical_name=canonical_name,
|
|
216
|
+
aliases=all_aliases,
|
|
217
|
+
overwrite=overwrite,
|
|
218
|
+
),
|
|
211
219
|
)
|
|
212
220
|
|
|
213
221
|
return decorator
|
|
@@ -297,6 +305,17 @@ class CodingAgent(BaseCodingAgent):
|
|
|
297
305
|
def event_handler(self, value: Any | None) -> None:
|
|
298
306
|
self._backend.event_handler = value
|
|
299
307
|
|
|
308
|
+
@property
|
|
309
|
+
def last_usage(self) -> Any:
|
|
310
|
+
"""Token/turn usage from the backend's most recent ``generate``.
|
|
311
|
+
|
|
312
|
+
``generate`` writes usage onto the concrete backend, so the
|
|
313
|
+
portable wrapper must surface it here — otherwise callers holding a
|
|
314
|
+
``CodingAgent`` (rather than the raw provider class) read a missing
|
|
315
|
+
attribute and lose all usage accounting.
|
|
316
|
+
"""
|
|
317
|
+
return getattr(self._backend, "last_usage", None)
|
|
318
|
+
|
|
300
319
|
def start_session(
|
|
301
320
|
self,
|
|
302
321
|
cwd: str | None = None,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
3
|
from collections.abc import Callable, Iterable, Sequence
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, cast
|
|
5
5
|
|
|
6
6
|
from ..base import register_provider
|
|
7
7
|
from ..cli_agent import CLICodingAgent, CLIGenerationSession
|
|
@@ -22,6 +22,10 @@ from .events import (
|
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _tool_args(value: Any) -> dict[str, Any]:
|
|
26
|
+
return cast("dict[str, Any]", value) if isinstance(value, dict) else {}
|
|
27
|
+
|
|
28
|
+
|
|
25
29
|
class CodexGenerationSession(CLIGenerationSession):
|
|
26
30
|
"""Session that parses Codex ``--json`` event stream."""
|
|
27
31
|
|
|
@@ -128,7 +132,7 @@ class CodexGenerationSession(CLIGenerationSession):
|
|
|
128
132
|
if event.tool_name is None:
|
|
129
133
|
return
|
|
130
134
|
event.tool_name_resolved = event.tool_name
|
|
131
|
-
args = event.parameters
|
|
135
|
+
args = _tool_args(event.parameters)
|
|
132
136
|
duration = None
|
|
133
137
|
if self.event_handler:
|
|
134
138
|
self.event_handler.on_tool_call(event.tool_name, args)
|
|
@@ -140,7 +144,7 @@ class CodexGenerationSession(CLIGenerationSession):
|
|
|
140
144
|
|
|
141
145
|
start_time = self.tool_start_times.get(event.tool_id)
|
|
142
146
|
duration = time.time() - start_time if start_time else None
|
|
143
|
-
args = self.tool_args.get(event.tool_id, event.parameters
|
|
147
|
+
args = _tool_args(self.tool_args.get(event.tool_id, event.parameters))
|
|
144
148
|
|
|
145
149
|
if event.tool_id not in self.tool_map and self.event_handler and event.tool_name:
|
|
146
150
|
self.event_handler.on_tool_call(event.tool_name, args)
|
|
@@ -104,15 +104,6 @@ def _item_parameters(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
104
104
|
return {k: v for k, v in item.items() if k not in excluded}
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
def _summarize_item(item: dict[str, Any]) -> str:
|
|
108
|
-
"""Summarize a generic codex item for the tool-result output field."""
|
|
109
|
-
for key in ("text", "summary", "output", "result"):
|
|
110
|
-
value = item.get(key)
|
|
111
|
-
if isinstance(value, str) and value:
|
|
112
|
-
return value
|
|
113
|
-
return ""
|
|
114
|
-
|
|
115
|
-
|
|
116
107
|
class ThreadStartedEvent(CodexEvent):
|
|
117
108
|
"""Thread-level start event carrying the resumable ``thread_id``."""
|
|
118
109
|
|
|
@@ -2,15 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import time
|
|
5
|
-
from
|
|
6
|
-
from typing import Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
6
|
|
|
8
7
|
from ..base import register_provider
|
|
9
8
|
from ..cli_agent import CLICodingAgent, CLIGenerationSession
|
|
10
|
-
from ..
|
|
11
|
-
from ..executor import CommandExecutor, CommandHandle
|
|
12
|
-
from ..mcp_config import HttpMcpServer, McpServerConfig
|
|
13
|
-
from ..sandbox import SandboxConfig
|
|
9
|
+
from ..mcp_config import HttpMcpServer
|
|
14
10
|
from ..usage import ProviderUsage, TokenUsage
|
|
15
11
|
from .events import (
|
|
16
12
|
CopilotEvent,
|
|
@@ -25,6 +21,14 @@ from .events import (
|
|
|
25
21
|
UsageEvent,
|
|
26
22
|
)
|
|
27
23
|
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
26
|
+
|
|
27
|
+
from ..events import AgentEventHandler
|
|
28
|
+
from ..executor import CommandExecutor, CommandHandle
|
|
29
|
+
from ..mcp_config import McpServerConfig
|
|
30
|
+
from ..sandbox import SandboxConfig
|
|
31
|
+
|
|
28
32
|
|
|
29
33
|
class CopilotGenerationSession(CLIGenerationSession):
|
|
30
34
|
"""Session that parses Copilot CLI JSONL events."""
|
|
@@ -71,6 +75,7 @@ class CopilotGenerationSession(CLIGenerationSession):
|
|
|
71
75
|
|
|
72
76
|
if not isinstance(data, dict):
|
|
73
77
|
return
|
|
78
|
+
data = cast("dict[str, Any]", data)
|
|
74
79
|
|
|
75
80
|
event = CopilotEvent.from_dict(data)
|
|
76
81
|
if event is None:
|
|
@@ -165,10 +170,9 @@ class CopilotGenerationSession(CLIGenerationSession):
|
|
|
165
170
|
self.session_id = event.session_id
|
|
166
171
|
return
|
|
167
172
|
|
|
168
|
-
if isinstance(event, ErrorEvent):
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
self.event_handler.on_thinking(f"[copilot error] {event.message}")
|
|
173
|
+
if isinstance(event, ErrorEvent) and event.message:
|
|
174
|
+
self.stdout_lines.append(event.message)
|
|
175
|
+
self.event_handler.on_thinking(f"[copilot error] {event.message}")
|
|
172
176
|
|
|
173
177
|
def run(self, prompt: str) -> str:
|
|
174
178
|
super().run(prompt)
|
|
@@ -20,8 +20,9 @@ def _extract_tool_result(result_raw: Any) -> tuple[str, int | None]:
|
|
|
20
20
|
|
|
21
21
|
contents = result.get("contents")
|
|
22
22
|
if isinstance(contents, list):
|
|
23
|
+
typed_contents = cast("list[Any]", contents)
|
|
23
24
|
rendered_blocks: list[str] = []
|
|
24
|
-
for item_raw in
|
|
25
|
+
for item_raw in typed_contents:
|
|
25
26
|
item = _as_dict(item_raw)
|
|
26
27
|
item_type = item.get("type")
|
|
27
28
|
if item_type in ("text", "terminal"):
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import io
|
|
4
3
|
import os
|
|
5
4
|
import shutil
|
|
6
5
|
import signal
|
|
7
6
|
import subprocess
|
|
8
7
|
import threading
|
|
9
|
-
from collections.abc import Callable
|
|
10
8
|
from dataclasses import dataclass
|
|
11
|
-
from typing import Any, Protocol
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import io
|
|
13
|
+
from collections.abc import Callable
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class CommandHandle(Protocol):
|
|
@@ -48,7 +50,7 @@ class CommandRequest:
|
|
|
48
50
|
stdin: str
|
|
49
51
|
cwd: str | None
|
|
50
52
|
env: dict[str, str]
|
|
51
|
-
timeout:
|
|
53
|
+
timeout: float | None
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
@dataclass(frozen=True)
|
|
@@ -230,9 +232,7 @@ class HostCommandExecutor:
|
|
|
230
232
|
f"Stderr: {result.stderr}"
|
|
231
233
|
)
|
|
232
234
|
except self._subprocess.TimeoutExpired as e:
|
|
233
|
-
raise RuntimeError(
|
|
234
|
-
f"CLI tool at '{binary_path}' did not respond to '--help' within {timeout}s."
|
|
235
|
-
) from e
|
|
235
|
+
raise RuntimeError(f"CLI tool at '{binary_path}' did not respond to '--help' within {timeout}s.") from e
|
|
236
236
|
except FileNotFoundError as e:
|
|
237
237
|
raise RuntimeError(
|
|
238
238
|
f"CLI tool not found at '{binary_path}'. Please ensure it is installed and in your PATH."
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
Install development dependencies and run the test suite with `uv`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uv sync --dev
|
|
7
|
+
uv run pytest
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Build Package
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv build
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Publish Package
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv publish
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Build Docs Locally
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv sync --group docs
|
|
26
|
+
uv run mkdocs build --strict
|
|
27
|
+
```
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Events
|
|
2
|
+
|
|
3
|
+
`agentshim` normalizes provider-specific streaming output into a small,
|
|
4
|
+
portable event interface. Claude Code, Codex, Copilot, Gemini, and Opencode all
|
|
5
|
+
emit different native event streams; the event handler API gives callers one
|
|
6
|
+
place to observe the common concepts across those tools.
|
|
7
|
+
|
|
8
|
+
Use events when you want to render live progress, capture tool activity, record
|
|
9
|
+
usage, or connect agent runs to your own logging or tracing system without
|
|
10
|
+
parsing each provider's CLI output yourself.
|
|
11
|
+
|
|
12
|
+
## Portable Events
|
|
13
|
+
|
|
14
|
+
Custom handlers can implement these common callbacks:
|
|
15
|
+
|
|
16
|
+
| Event | Called when | Notes |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `on_thinking(text)` | The agent emits assistant text, reasoning/progress text, or other provider status text. | Text may arrive incrementally. Handlers should append or stream it rather than assuming one complete message per call. |
|
|
19
|
+
| `on_tool_call(tool, args=None)` | The provider reports that the agent is starting or requesting a tool call. | `tool` is the provider's tool name. `args` is usually a dict, but can be a string or `None` when that is all the provider exposes. |
|
|
20
|
+
| `on_tool_result(tool, stdout="", stderr="", exit_code=None, duration=None)` | The provider reports a completed tool result. | Providers vary in how much result metadata they expose, so handlers should tolerate missing `exit_code` and `duration`. |
|
|
21
|
+
| `on_usage(usage)` | The provider reports per-turn token or usage information. | The payload is a normalized best-effort dict. Older handlers can omit this method. |
|
|
22
|
+
|
|
23
|
+
These events are intentionally broad. They are not a lossless copy of every
|
|
24
|
+
provider's native event schema; they are the stable interface for common
|
|
25
|
+
observability and UI use cases across coding-agent CLIs.
|
|
26
|
+
|
|
27
|
+
## Optional Runtime Hooks
|
|
28
|
+
|
|
29
|
+
Some built-in handlers also understand lower-level runtime hooks:
|
|
30
|
+
|
|
31
|
+
| Hook | Called when |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `on_run_start(command)` | The underlying provider CLI command is about to start. |
|
|
34
|
+
| `on_run_end(exit_code=None)` | The underlying provider CLI command has exited. |
|
|
35
|
+
| `on_stderr(text)` | The provider process writes stderr that is not parsed as a structured provider event. |
|
|
36
|
+
|
|
37
|
+
These hooks are useful for terminal renderers and debugging. They are optional:
|
|
38
|
+
custom handlers focused on portable agent events can ignore them.
|
|
39
|
+
|
|
40
|
+
## Default Console Output
|
|
41
|
+
|
|
42
|
+
By default, `agentshim` prints provider events to the terminal through a
|
|
43
|
+
`ConsoleEventHandler`. That default is used only when you do not provide your
|
|
44
|
+
own event handler and `silent=False`.
|
|
45
|
+
|
|
46
|
+
If you pass `event_handler=...`, you take ownership of event handling. The
|
|
47
|
+
built-in console printer is not added implicitly, which avoids surprising
|
|
48
|
+
duplicate output.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from agentshim import CodingAgent
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MyHandler:
|
|
55
|
+
def on_thinking(self, text: str) -> None:
|
|
56
|
+
print(text, end="")
|
|
57
|
+
|
|
58
|
+
def on_tool_call(self, tool: str, args: dict | str | None = None) -> None:
|
|
59
|
+
print(f"tool started: {tool} {args or ''}")
|
|
60
|
+
|
|
61
|
+
def on_tool_result(
|
|
62
|
+
self,
|
|
63
|
+
tool: str,
|
|
64
|
+
stdout: str = "",
|
|
65
|
+
stderr: str = "",
|
|
66
|
+
exit_code: int | None = None,
|
|
67
|
+
duration: float | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
output = stdout or stderr
|
|
70
|
+
print(f"tool finished: {tool} exit={exit_code} {output}")
|
|
71
|
+
|
|
72
|
+
def on_usage(self, usage: dict) -> None:
|
|
73
|
+
print(f"usage: {usage}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
agent = CodingAgent(provider="claude", event_handler=MyHandler())
|
|
77
|
+
agent.generate("Inspect this repository.")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
You can implement only the events your application needs when the provider path
|
|
81
|
+
checks for optional callbacks. For maximum compatibility across providers and
|
|
82
|
+
handler composition, define all four portable event methods.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
class MinimalHandler:
|
|
86
|
+
def on_thinking(self, text: str) -> None:
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
def on_tool_call(self, tool: str, args: dict | str | None = None) -> None:
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
def on_tool_result(
|
|
93
|
+
self,
|
|
94
|
+
tool: str,
|
|
95
|
+
stdout: str = "",
|
|
96
|
+
stderr: str = "",
|
|
97
|
+
exit_code: int | None = None,
|
|
98
|
+
duration: float | None = None,
|
|
99
|
+
) -> None:
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
def on_usage(self, usage: dict) -> None:
|
|
103
|
+
...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
To keep the default console output and add your own handler, compose them
|
|
107
|
+
explicitly.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from agentshim import CodingAgent, ConsoleEventHandler
|
|
111
|
+
|
|
112
|
+
agent = CodingAgent(
|
|
113
|
+
provider="claude",
|
|
114
|
+
event_handlers=[
|
|
115
|
+
ConsoleEventHandler(),
|
|
116
|
+
MyHandler(),
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
agent.generate("Inspect this repository.")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
You can also build the composition yourself.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from agentshim import CompositeEventHandler, ConsoleEventHandler
|
|
126
|
+
|
|
127
|
+
handler = CompositeEventHandler([ConsoleEventHandler(), MyHandler()])
|
|
128
|
+
agent = CodingAgent(provider="codex", event_handler=handler)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use `silent=True` to suppress the default console handler when you have not
|
|
132
|
+
provided any handler.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
agent = CodingAgent(provider="claude")
|
|
136
|
+
reply = agent.generate("Return only the answer.", silent=True)
|
|
137
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Executors
|
|
2
|
+
|
|
3
|
+
Provider classes accept an optional `executor=`. The executor controls binary
|
|
4
|
+
lookup, CLI validation, and streaming process execution while `agentshim` keeps
|
|
5
|
+
owning provider command construction, stdout/stderr parsing, session state, and
|
|
6
|
+
event emission.
|
|
7
|
+
|
|
8
|
+
This is useful when a caller needs to run the CLI somewhere other than the
|
|
9
|
+
current host process, for example through an existing container, remote shell,
|
|
10
|
+
or custom sandbox.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from agentshim import (
|
|
14
|
+
CodexCodingAgent,
|
|
15
|
+
CommandHandle,
|
|
16
|
+
CommandRequest,
|
|
17
|
+
CommandResult,
|
|
18
|
+
CommandStreamSink,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MyCommandHandle:
|
|
23
|
+
def terminate(self) -> None:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
def kill(self) -> None:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MyExecutor:
|
|
31
|
+
def find_binary(self, binary_name: str, env: dict[str, str]) -> str:
|
|
32
|
+
# This value becomes request.argv[0]. Container or remote executors can
|
|
33
|
+
# return the binary name if lookup happens in the target runtime.
|
|
34
|
+
return binary_name
|
|
35
|
+
|
|
36
|
+
def check_binary(self, binary_path: str, env: dict[str, str], *, timeout: int) -> None:
|
|
37
|
+
# Raise RuntimeError if the target CLI is unavailable. No-op is fine
|
|
38
|
+
# when validation is not cheap or is handled by the runtime.
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def run(self, request: CommandRequest, sink: CommandStreamSink) -> CommandResult:
|
|
42
|
+
handle = MyCommandHandle()
|
|
43
|
+
sink.started(handle)
|
|
44
|
+
|
|
45
|
+
stdout = ""
|
|
46
|
+
stderr = ""
|
|
47
|
+
|
|
48
|
+
# Run request.argv in your target runtime, with request.stdin,
|
|
49
|
+
# request.cwd, request.env, and request.timeout. Stream each complete
|
|
50
|
+
# line as it arrives, preserving trailing newlines when present.
|
|
51
|
+
line = "streamed output\n"
|
|
52
|
+
stdout += line
|
|
53
|
+
sink.stdout(line)
|
|
54
|
+
|
|
55
|
+
return CommandResult(returncode=0, stdout=stdout, stderr=stderr)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
agent = CodexCodingAgent(executor=MyExecutor())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The default `HostCommandExecutor` preserves the normal local `subprocess`
|
|
62
|
+
behavior. `CodingAgent(provider=..., executor=...)` forwards the same executor
|
|
63
|
+
to the selected provider.
|
|
64
|
+
|
|
65
|
+
Executor contract:
|
|
66
|
+
|
|
67
|
+
- `CommandRequest.argv` is the complete provider CLI command. `argv[0]` is the
|
|
68
|
+
value returned by `find_binary`.
|
|
69
|
+
- `CommandRequest.stdin` is the prompt text to write to the command's standard
|
|
70
|
+
input, then stdin should be closed.
|
|
71
|
+
- `CommandRequest.cwd`, `env`, and `timeout` should be honored by the executor.
|
|
72
|
+
- Call `sink.started(handle)` once after the command starts. The handle only
|
|
73
|
+
needs `terminate()` and `kill()`.
|
|
74
|
+
- Call `sink.stdout(line)` and `sink.stderr(line)` as output is produced. Lines
|
|
75
|
+
should include trailing newlines when the underlying stream provided them.
|
|
76
|
+
- Return `CommandResult(returncode, stdout, stderr)` after the command exits.
|
|
77
|
+
The returned text should match what was streamed through the sink.
|
|
78
|
+
|
|
79
|
+
If you were using the pre-0.5 executor preview, replace
|
|
80
|
+
`run_streaming(cmd, ..., on_stdout, on_stderr, on_process_started)` with
|
|
81
|
+
`run(request, sink)`. The parser/event APIs remain internal; custom executors
|
|
82
|
+
only provide a stable command runtime.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Extending agentshim
|
|
2
|
+
|
|
3
|
+
Advanced users can register their own providers. `CodingAgent(...)` keeps its
|
|
4
|
+
main constructor portable; provider-specific constructor extras should go
|
|
5
|
+
through `backend_kwargs`.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from agentshim import BaseCodingAgent, CodingAgent, register_provider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register_provider("my-agent", aliases=("my-agent-dev",))
|
|
12
|
+
class MyAgent(BaseCodingAgent):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
model: str | None = None,
|
|
16
|
+
region: str | None = None,
|
|
17
|
+
event_handler=None,
|
|
18
|
+
event_handlers=None,
|
|
19
|
+
mcp_servers=None,
|
|
20
|
+
sandbox=False,
|
|
21
|
+
):
|
|
22
|
+
self.model = model
|
|
23
|
+
self.region = region
|
|
24
|
+
self.event_handler = event_handler
|
|
25
|
+
|
|
26
|
+
def generate(self, prompt: str, cwd=None, timeout=300, silent=False) -> str:
|
|
27
|
+
return f"handled: {prompt}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
agent = CodingAgent(
|
|
31
|
+
provider="my-agent-dev",
|
|
32
|
+
model="demo",
|
|
33
|
+
backend_kwargs={"region": "us-west1"},
|
|
34
|
+
)
|
|
35
|
+
print(agent.generate("hello"))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Notes:
|
|
39
|
+
|
|
40
|
+
- Registration is import-driven. Your provider is available only after the
|
|
41
|
+
module defining it has been imported in the current Python process.
|
|
42
|
+
- `list_providers()` returns canonical provider names only. Aliases resolve via
|
|
43
|
+
`get_provider_class(...)` and `CodingAgent(provider=...)`.
|
|
44
|
+
- `register_provider(...)` rejects invalid names, abstract classes, and
|
|
45
|
+
accidental name collisions unless you pass `overwrite=True`.
|
|
46
|
+
- If you want `CodingAgent(...)` to instantiate your provider, its constructor
|
|
47
|
+
should accept the shared kwargs `model`, `event_handler`, `event_handlers`,
|
|
48
|
+
`mcp_servers`, `sandbox`, and `executor` as needed.
|
|
49
|
+
- If your provider needs extra constructor arguments beyond the shared portable
|
|
50
|
+
set, pass them via `backend_kwargs={...}` when constructing
|
|
51
|
+
`CodingAgent(...)`.
|