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.
Files changed (81) hide show
  1. {agentshim-0.5.0 → agentshim-0.5.1}/.github/workflows/ci.yml +16 -1
  2. agentshim-0.5.1/.github/workflows/docs.yml +52 -0
  3. {agentshim-0.5.0 → agentshim-0.5.1}/.gitignore +1 -0
  4. {agentshim-0.5.0 → agentshim-0.5.1}/PKG-INFO +4 -1
  5. {agentshim-0.5.0 → agentshim-0.5.1}/README.md +3 -0
  6. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/base.py +32 -13
  7. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/codex/agent.py +7 -3
  8. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/codex/events.py +0 -9
  9. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/copilot/agent.py +14 -10
  10. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/copilot/events.py +2 -1
  11. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/executor.py +7 -7
  12. agentshim-0.5.1/docs/api.md +3 -0
  13. agentshim-0.5.1/docs/development.md +27 -0
  14. agentshim-0.5.1/docs/events.md +137 -0
  15. agentshim-0.5.1/docs/executors.md +82 -0
  16. agentshim-0.5.1/docs/extending.md +51 -0
  17. agentshim-0.5.1/docs/getting-started.md +45 -0
  18. agentshim-0.5.1/docs/index.md +37 -0
  19. agentshim-0.5.1/docs/mcp.md +35 -0
  20. agentshim-0.5.1/docs/providers.md +23 -0
  21. agentshim-0.5.1/mkdocs.yml +43 -0
  22. {agentshim-0.5.0 → agentshim-0.5.1}/pyproject.toml +9 -2
  23. agentshim-0.5.1/pyrightconfig.json +10 -0
  24. agentshim-0.5.1/scripts/check_errors.sh +20 -0
  25. agentshim-0.5.1/scripts/format_code.sh +19 -0
  26. agentshim-0.5.1/scripts/type_check.sh +17 -0
  27. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/cli_agent/test_command_executor.py +4 -6
  28. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/llm/test_claude_stream.py +2 -1
  29. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_event_parsing.py +2 -1
  30. agentshim-0.5.1/tests/unit/test_claude_usage_e2e.py +105 -0
  31. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_coding_agent_facade.py +10 -1
  32. {agentshim-0.5.0 → agentshim-0.5.1}/uv.lock +321 -0
  33. {agentshim-0.5.0 → agentshim-0.5.1}/.github/workflows/publish.yml +0 -0
  34. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/__init__.py +1 -1
  35. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/__init__.py +0 -0
  36. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/agent.py +0 -0
  37. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/events.py +0 -0
  38. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/hooks/__init__.py +0 -0
  39. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude/hooks/confine_reads.py +0 -0
  40. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/claude_events.py +0 -0
  41. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/cli_agent.py +1 -1
  42. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/codex/__init__.py +0 -0
  43. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/codex_events.py +0 -0
  44. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/copilot/__init__.py +0 -0
  45. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/copilot_events.py +0 -0
  46. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/events.py +0 -0
  47. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/gemini/__init__.py +0 -0
  48. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/gemini/agent.py +0 -0
  49. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/gemini/events.py +0 -0
  50. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/gemini_events.py +0 -0
  51. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/llm_client.py +0 -0
  52. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/mcp_config.py +0 -0
  53. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/opencode/__init__.py +0 -0
  54. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/opencode/agent.py +0 -0
  55. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/opencode/events.py +0 -0
  56. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/opencode_events.py +0 -0
  57. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/py.typed +0 -0
  58. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/sandbox.py +0 -0
  59. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/subagent.py +0 -0
  60. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/usage.py +0 -0
  61. {agentshim-0.5.0 → agentshim-0.5.1}/agentshim/utils.py +0 -0
  62. {agentshim-0.5.0 → agentshim-0.5.1}/tests/fixtures/copilot/session_turn_1.jsonl +0 -0
  63. {agentshim-0.5.0 → agentshim-0.5.1}/tests/fixtures/copilot/session_turn_2_resumed.jsonl +0 -0
  64. {agentshim-0.5.0 → agentshim-0.5.1}/tests/fixtures/copilot/streaming_dedup.jsonl +0 -0
  65. {agentshim-0.5.0 → agentshim-0.5.1}/tests/fixtures/copilot/tool_and_usage.jsonl +0 -0
  66. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/cli_agent/test_agent_cli_cleanup.py +0 -0
  67. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/cli_agent/test_check_cli.py +0 -0
  68. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/cli_agent/test_cli_prompt_passing.py +1 -1
  69. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/llm/conftest.py +0 -0
  70. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/llm/test_gemini_fixture.py +0 -0
  71. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/llm/test_gemini_stream.py +0 -0
  72. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_claude.py +0 -0
  73. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_codex.py +0 -0
  74. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_copilot.py +0 -0
  75. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_copilot_fixtures.py +0 -0
  76. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_mcp_unsupported.py +0 -0
  77. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_resume.py +2 -2
  78. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_agent_cli_sandbox.py +0 -0
  79. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_cli_agent_usage.py +2 -2
  80. {agentshim-0.5.0 → agentshim-0.5.1}/tests/unit/test_event_handlers.py +0 -0
  81. {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: uv run ruff check .
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
@@ -4,3 +4,4 @@ __pycache__/
4
4
  .ruff_cache/
5
5
  dist/
6
6
  build/
7
+ site/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentshim
3
- Version: 0.5.0
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
 
@@ -331,6 +331,9 @@ Notes:
331
331
 
332
332
  ```bash
333
333
  uv sync --dev
334
+ bash scripts/format_code.sh --check
335
+ bash scripts/check_errors.sh
336
+ bash scripts/type_check.sh
334
337
  uv run pytest
335
338
  ```
336
339
 
@@ -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 collections.abc import Callable, Sequence
7
- from typing import Any, TypeVar
6
+ from typing import TYPE_CHECKING, Any, TypeVar, cast
8
7
 
9
- from agentshim.events import AgentEventHandler
10
- from agentshim.executor import CommandExecutor, CommandHandle
11
- from agentshim.mcp_config import McpServerConfig
12
- from agentshim.sandbox import SandboxConfig
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: str) -> str:
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._normalize_names(canonical_name, all_aliases)
208
+ _PROVIDER_REGISTRY.validate_names(canonical_name, all_aliases)
204
209
 
205
210
  def decorator(cls: type[_T]) -> type[_T]:
206
- return _PROVIDER_REGISTRY.register(
207
- cls,
208
- canonical_name=canonical_name,
209
- aliases=all_aliases,
210
- overwrite=overwrite,
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 or {}
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 or {})
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 collections.abc import Callable, Iterable, Sequence
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 ..events import AgentEventHandler
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
- if event.message:
170
- self.stdout_lines.append(event.message)
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 contents:
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: int | None
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,3 @@
1
+ # API Reference
2
+
3
+ ::: agentshim
@@ -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(...)`.