agent-shell-py 0.1.17__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_shell_py-0.1.17 → agent_shell_py-0.2.0}/AGENTS.md +25 -5
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/PKG-INFO +19 -3
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/README.md +18 -2
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/agent-skills/delegating-code-review/SKILL.md +41 -9
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/agent-skills/invoking-cli-agents/SKILL.md +47 -30
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/agent-skills/invoking-cli-agents/api-reference.md +66 -9
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/docs/development/agent_parameter_comparison.md +67 -37
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/docs/development/disabled_tools.md +0 -1
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/docs/development/total_token_count.md +5 -1
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/_version.py +2 -2
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/agent_adapter_protocol.py +7 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/claude_code_adapter.py +27 -24
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/codex_adapter.py +27 -24
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/copilot_cli_adapter.py +27 -24
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/cursor_adapter.py +27 -24
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/health.py +15 -22
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/opencode_adapter.py +27 -24
- agent_shell_py-0.2.0/src/agent_shell/adapters/outcome.py +68 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/pi_adapter.py +74 -33
- agent_shell_py-0.2.0/src/agent_shell/adapters/response.py +70 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/models/agent.py +34 -1
- agent_shell_py-0.2.0/src/agent_shell/process_cleanup.py +213 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/shell.py +14 -10
- agent_shell_py-0.2.0/tests/conftest.py +50 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/e2e/test_claude_code_e2e.py +22 -11
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/e2e/test_codex_e2e.py +20 -9
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/e2e/test_copilot_cli_e2e.py +67 -12
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/e2e/test_cursor_e2e.py +26 -10
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/e2e/test_health_check_e2e.py +1 -1
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/e2e/test_opencode_e2e.py +22 -10
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/e2e/test_pi_e2e.py +28 -9
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_health_check_integration.py +5 -3
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_pi_integration.py +3 -1
- agent_shell_py-0.2.0/tests/integration/test_process_lifecycle.py +283 -0
- agent_shell_py-0.2.0/tests/unit/adapter_matrix.py +67 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/opencode_fixtures.py +10 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/pi_fixtures.py +103 -3
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_adapter_transport.py +223 -6
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_cursor_execute.py +16 -8
- agent_shell_py-0.2.0/tests/unit/test_execute_outcome.py +446 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_health_probe.py +17 -2
- agent_shell_py-0.2.0/tests/unit/test_models.py +97 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_pi_execute.py +15 -7
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_pi_parse_event.py +132 -7
- agent_shell_py-0.2.0/tests/unit/test_process_cleanup.py +596 -0
- agent_shell_py-0.2.0/tests/unit/test_response_aggregation.py +131 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_shell.py +16 -3
- agent_shell_py-0.1.17/src/agent_shell/process_cleanup.py +0 -54
- agent_shell_py-0.1.17/tests/unit/test_models.py +0 -33
- agent_shell_py-0.1.17/tests/unit/test_process_cleanup.py +0 -137
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/.github/workflows/build.yml +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/.github/workflows/ci.yml +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/.github/workflows/publish.yml +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/.gitignore +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/.python-version +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/LICENSE +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/docs/development/info.md +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/pyproject.toml +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/__init__.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/__init__.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/stderr_format.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/adapters/tool_denial.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/src/agent_shell/models/__init__.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/__init__.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/e2e/__init__.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/__init__.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_claude_code_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_claude_code_mcp_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_codex_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_codex_mcp_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_copilot_cli_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_copilot_cli_mcp_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_cursor_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_cursor_mcp_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_opencode_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_opencode_mcp_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/integration/test_pi_mcp_integration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/__init__.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/codex_fixtures.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/copilot_fixtures.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/cursor_fixtures.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/fixtures.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_cancel.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_codex_cancel.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_codex_execute.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_codex_parse_event.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_codex_warnings.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_cancel.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_execute.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_parse_event.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_stream.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_cursor_cancel.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_cursor_parse_event.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_cursor_warnings.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_execute.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_mcp_server_spec.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_opencode_cancel.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_opencode_execute.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_opencode_parse_event.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_opencode_spawn.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_opencode_stream.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_parse_event.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_pi_cancel.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_pi_warnings.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_process_group_registration.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_shell_cancellation.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_shell_mcp.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_stderr_format.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_stream.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/tests/unit/test_tool_denial.py +0 -0
- {agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/uv.lock +0 -0
|
@@ -38,6 +38,16 @@ classDiagram
|
|
|
38
38
|
-_parse_event(event, include_thinking) list~StreamEvent~
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
class AgentExecutionError {
|
|
42
|
+
<<Exception>>
|
|
43
|
+
+str reason
|
|
44
|
+
+str response
|
|
45
|
+
+float cost
|
|
46
|
+
+str session_id
|
|
47
|
+
+float duration
|
|
48
|
+
+int output_tokens
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
class MCPServerSpec {
|
|
42
52
|
+str name
|
|
43
53
|
+MCPServerType type
|
|
@@ -74,13 +84,13 @@ classDiagram
|
|
|
74
84
|
+float duration
|
|
75
85
|
+str session_id
|
|
76
86
|
+int output_tokens
|
|
87
|
+
+str error
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
class AgentType {
|
|
80
91
|
<<StrEnum>>
|
|
81
92
|
CLAUDE_CODE
|
|
82
93
|
OPENCODE
|
|
83
|
-
GEMINI_CLI
|
|
84
94
|
COPILOT_CLI
|
|
85
95
|
CODEX
|
|
86
96
|
PI
|
|
@@ -90,7 +100,8 @@ classDiagram
|
|
|
90
100
|
AgentShell --> AgentAdapter : delegates to
|
|
91
101
|
AgentShell --> AgentType : resolves via
|
|
92
102
|
ClaudeCodeAdapter ..|> AgentAdapter : satisfies
|
|
93
|
-
AgentShell ..> AgentResponse : returns
|
|
103
|
+
AgentShell ..> AgentResponse : returns on success
|
|
104
|
+
AgentShell ..> AgentExecutionError : raises on failure
|
|
94
105
|
AgentShell ..> HealthCheckResult : returns
|
|
95
106
|
AgentShell ..> StreamEvent : yields
|
|
96
107
|
AgentShell ..> MCPServerSpec : accepts/returns
|
|
@@ -102,7 +113,17 @@ The adapter pattern uses Python's `Protocol` (structural typing) rather than ABC
|
|
|
102
113
|
|
|
103
114
|
`output_tokens` is a cost measure — the billed output-token count, which **includes reasoning tokens** (billed at the output rate). Each adapter normalises this so the value is consistent across agents (e.g. OpenCode reports reasoning in a sibling field, so its adapter adds it back).
|
|
104
115
|
|
|
105
|
-
`health_check(cwd, model, timeout)` probes an agent + model combination with a trivial prompt
|
|
116
|
+
`health_check(cwd, model, timeout)` probes an agent + model combination with a trivial prompt
|
|
117
|
+
and returns `HealthCheckResult(healthy, exception)`. The verdict is derived from the normalised
|
|
118
|
+
event stream (healthy = the LAST `result` event says `content == "ok"` and no `error` event
|
|
119
|
+
arrived — pi emits one `result` per agent loop, and its auto-retry runs more than one), not exit
|
|
120
|
+
codes — which are unreliable, since some CLIs exit 0 on failure. `execute()` judges a run by the
|
|
121
|
+
same rule and raises `AgentExecutionError` when it fails, carrying whatever partial
|
|
122
|
+
response/cost/session/token data the run produced. The success/failure verdict lives once in
|
|
123
|
+
`adapters/outcome.py`, shared by both surfaces so they report identical reasons for the same
|
|
124
|
+
stream; `adapters/health.py` wraps it for the health probe, and `adapters/response.py` wraps it
|
|
125
|
+
for `execute()`'s stream-to-`AgentResponse` collection (used by every adapter — each `execute()`
|
|
126
|
+
is a single delegating call into it).
|
|
106
127
|
|
|
107
128
|
## Supported Agents
|
|
108
129
|
|
|
@@ -112,14 +133,13 @@ The adapter pattern uses Python's `Protocol` (structural typing) rather than ABC
|
|
|
112
133
|
- [x] Codex
|
|
113
134
|
- [x] Pi
|
|
114
135
|
- [x] Cursor
|
|
115
|
-
- [ ] Gemini CLI
|
|
116
136
|
|
|
117
137
|
## MCP Server Configuration
|
|
118
138
|
|
|
119
139
|
`AgentShell` exposes a unified API for registering MCP servers across all supported agents:
|
|
120
140
|
|
|
121
141
|
```python
|
|
122
|
-
from agent_shell import AgentShell
|
|
142
|
+
from agent_shell.shell import AgentShell
|
|
123
143
|
from agent_shell.models.agent import AgentType, MCPServerSpec, MCPServerType
|
|
124
144
|
|
|
125
145
|
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-shell-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A lightweight abstraction for executing CLI coding agents headlessly
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -17,8 +17,8 @@ and returning the output that can be used programatically as a unified contract
|
|
|
17
17
|
agent; swap the backend without changing a line of consuming code.
|
|
18
18
|
- **Six CLI agents** — Claude Code, OpenCode, Copilot CLI, Codex, Pi, and Cursor behind a
|
|
19
19
|
common adapter protocol.
|
|
20
|
-
- **Execute or stream** — get one `AgentResponse
|
|
21
|
-
with optional thinking/reasoning.
|
|
20
|
+
- **Execute or stream** — get one `AgentResponse` (raises `AgentExecutionError` on a failed run),
|
|
21
|
+
or async-iterate normalized `StreamEvent`s with optional thinking/reasoning.
|
|
22
22
|
- **Session resumption** — continue any conversation by passing back its `session_id`.
|
|
23
23
|
- **Normalized cost & tokens** — consistent `cost` and `output_tokens` (reasoning included)
|
|
24
24
|
regardless of how each CLI reports them.
|
|
@@ -77,6 +77,22 @@ follow_up = await shell.execute(
|
|
|
77
77
|
> `output_tokens` is a cost measure: the billed output-token count, which **includes reasoning
|
|
78
78
|
> tokens** (they are billed at the output rate). It is reported consistently across all adapters.
|
|
79
79
|
|
|
80
|
+
### Failure handling
|
|
81
|
+
|
|
82
|
+
`execute()` raises `AgentExecutionError` instead of returning when a run failed — an `error`
|
|
83
|
+
event was emitted, the terminal `result` had `content == "error"`, or no terminal `result`
|
|
84
|
+
arrived at all. `str(e)` is the bare reason; the exception also carries whatever partial
|
|
85
|
+
`response`/`cost`/`session_id`/`duration`/`output_tokens` the run produced before failing.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from agent_shell.models.agent import AgentExecutionError
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
response = await shell.execute(cwd="/path/to/project", prompt="Fix the failing test")
|
|
92
|
+
except AgentExecutionError as e:
|
|
93
|
+
print(f"run failed: {e}") # e.g. "500 model name=qwen3.6-27b-8Q failed to load"
|
|
94
|
+
```
|
|
95
|
+
|
|
80
96
|
### Stream
|
|
81
97
|
|
|
82
98
|
```python
|
|
@@ -8,8 +8,8 @@ and returning the output that can be used programatically as a unified contract
|
|
|
8
8
|
agent; swap the backend without changing a line of consuming code.
|
|
9
9
|
- **Six CLI agents** — Claude Code, OpenCode, Copilot CLI, Codex, Pi, and Cursor behind a
|
|
10
10
|
common adapter protocol.
|
|
11
|
-
- **Execute or stream** — get one `AgentResponse
|
|
12
|
-
with optional thinking/reasoning.
|
|
11
|
+
- **Execute or stream** — get one `AgentResponse` (raises `AgentExecutionError` on a failed run),
|
|
12
|
+
or async-iterate normalized `StreamEvent`s with optional thinking/reasoning.
|
|
13
13
|
- **Session resumption** — continue any conversation by passing back its `session_id`.
|
|
14
14
|
- **Normalized cost & tokens** — consistent `cost` and `output_tokens` (reasoning included)
|
|
15
15
|
regardless of how each CLI reports them.
|
|
@@ -68,6 +68,22 @@ follow_up = await shell.execute(
|
|
|
68
68
|
> `output_tokens` is a cost measure: the billed output-token count, which **includes reasoning
|
|
69
69
|
> tokens** (they are billed at the output rate). It is reported consistently across all adapters.
|
|
70
70
|
|
|
71
|
+
### Failure handling
|
|
72
|
+
|
|
73
|
+
`execute()` raises `AgentExecutionError` instead of returning when a run failed — an `error`
|
|
74
|
+
event was emitted, the terminal `result` had `content == "error"`, or no terminal `result`
|
|
75
|
+
arrived at all. `str(e)` is the bare reason; the exception also carries whatever partial
|
|
76
|
+
`response`/`cost`/`session_id`/`duration`/`output_tokens` the run produced before failing.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from agent_shell.models.agent import AgentExecutionError
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
response = await shell.execute(cwd="/path/to/project", prompt="Fix the failing test")
|
|
83
|
+
except AgentExecutionError as e:
|
|
84
|
+
print(f"run failed: {e}") # e.g. "500 model name=qwen3.6-27b-8Q failed to load"
|
|
85
|
+
```
|
|
86
|
+
|
|
71
87
|
### Stream
|
|
72
88
|
|
|
73
89
|
```python
|
|
@@ -29,8 +29,17 @@ instruction in the prompt — restrict the tools. But mind how the two controls
|
|
|
29
29
|
The whitelist is *inert under the default `auto_approve=True`* (`--dangerously-skip-permissions`
|
|
30
30
|
auto-approves everything), so the `auto_approve=False` is what makes it bite:
|
|
31
31
|
`allowed_tools=["Read", "Glob", "Grep", "Bash"], auto_approve=False`.
|
|
32
|
-
- **OpenCode
|
|
33
|
-
`
|
|
32
|
+
- **OpenCode** — it ignores `allowed_tools`, but its denylist **is** genuinely enforced (a
|
|
33
|
+
per-subprocess `OPENCODE_PERMISSION` env var), and deny beats `auto_approve`, so keep the
|
|
34
|
+
default `auto_approve=True`: `disallowed_tools=["edit"]`.
|
|
35
|
+
- **Codex / Cursor** — **in-library tool scoping gives you nothing here.** They ignore
|
|
36
|
+
`allowed_tools`, *and* they cannot enforce `disallowed_tools=["edit"]`. Codex can only deny
|
|
37
|
+
`web_search`; anything else prints
|
|
38
|
+
`UserWarning: Codex can only deny web_search; ignoring ['edit']` and the reviewer then runs
|
|
39
|
+
with unrestricted write access. Cursor has no per-call deny at all (its tool policy lives in
|
|
40
|
+
`.cursor/cli.json`). If a Codex/Cursor reviewer must not write, enforce it outside the
|
|
41
|
+
library: capture `git diff` yourself and pass it in the prompt, run the reviewer under an
|
|
42
|
+
OS-level read-only sandbox, and/or use a different agent type when read-only really matters.
|
|
34
43
|
|
|
35
44
|
**This is defence-in-depth, not a sandbox.** Any reviewer that keeps `bash` (for `git diff`) can
|
|
36
45
|
still write via the shell, and in-library scoping doesn't cover MCP-provided tools. For a *hard*
|
|
@@ -60,6 +69,27 @@ Flag issues by severity: CRITICAL, WARNING, or SUGGESTION.""",
|
|
|
60
69
|
|
|
61
70
|
The reviewer will run `git diff` itself, read surrounding code for context, and report findings.
|
|
62
71
|
|
|
72
|
+
## Handling a Failed Review
|
|
73
|
+
|
|
74
|
+
A failed reviewer run raises `AgentExecutionError` instead of returning a response — catch it
|
|
75
|
+
so a crashed or rate-limited reviewer doesn't look like a silent "no issues found":
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from agent_shell.models.agent import AgentExecutionError
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
review = await reviewer.execute(
|
|
82
|
+
cwd="/path/to/project",
|
|
83
|
+
prompt="Review the uncommitted changes in this repository.",
|
|
84
|
+
allowed_tools=["Read", "Glob", "Grep", "Bash"],
|
|
85
|
+
auto_approve=False,
|
|
86
|
+
model="sonnet",
|
|
87
|
+
)
|
|
88
|
+
except AgentExecutionError as e:
|
|
89
|
+
print(f"review failed: {e}") # e.g. "500 model name=... failed to load"
|
|
90
|
+
raise
|
|
91
|
+
```
|
|
92
|
+
|
|
63
93
|
## Review a Specific Commit or Range
|
|
64
94
|
|
|
65
95
|
Point the reviewer at a particular changeset — only the prompt changes.
|
|
@@ -96,7 +126,9 @@ clarification = await reviewer.execute(
|
|
|
96
126
|
## Cross-Agent Review
|
|
97
127
|
|
|
98
128
|
Use a different agent or model than the one that wrote the code for genuine independence.
|
|
99
|
-
OpenCode ignores `allowed_tools`, so restrict it with `disallowed_tools` instead
|
|
129
|
+
OpenCode ignores `allowed_tools`, so restrict it with `disallowed_tools` instead — that deny
|
|
130
|
+
*is* enforced. Don't swap `AgentType.CODEX` or `AgentType.CURSOR` into this example expecting
|
|
131
|
+
the same protection: they drop the deny with only a `UserWarning`.
|
|
100
132
|
|
|
101
133
|
```python
|
|
102
134
|
reviewer = AgentShell(agent_type=AgentType.OPENCODE)
|
|
@@ -112,10 +144,9 @@ Flag issues by severity: CRITICAL, WARNING, or SUGGESTION.""",
|
|
|
112
144
|
```
|
|
113
145
|
|
|
114
146
|
> OpenCode reports `cost` as `0.0` for many models — use `output_tokens` if you need a usage
|
|
115
|
-
> figure. And
|
|
116
|
-
>
|
|
117
|
-
>
|
|
118
|
-
> core skill's Error Handling).
|
|
147
|
+
> figure. And a failed reviewer run raises `AgentExecutionError` rather than returning — wrap
|
|
148
|
+
> the call (see [Handling a Failed Review](#handling-a-failed-review) below), since OpenCode can
|
|
149
|
+
> truncate a review with no terminal event and no error (see the core skill's Error Handling).
|
|
119
150
|
|
|
120
151
|
## Prompt Patterns
|
|
121
152
|
|
|
@@ -155,9 +186,10 @@ For each gap, describe the test case that should exist and why it matters.
|
|
|
155
186
|
|---------|-----|
|
|
156
187
|
| Relying on a "do not modify files" prompt instruction for safety | Enforce it (whitelist + `auto_approve=False`, or `disallowed_tools`) |
|
|
157
188
|
| Whitelisting tools but leaving the default `auto_approve=True` | `--dangerously-skip-permissions` makes the whitelist inert — set `auto_approve=False` |
|
|
158
|
-
| Assuming `allowed_tools` restricts OpenCode
|
|
189
|
+
| Assuming `allowed_tools` restricts OpenCode, Codex, or Cursor | All three ignore it |
|
|
190
|
+
| `disallowed_tools` to keep a Codex/Cursor reviewer read-only | It only warns; sandbox instead |
|
|
159
191
|
| Believing `disallowed_tools=["edit"]` makes the reviewer read-only | It doesn't — the model writes via `bash`; also deny `bash` (and OS-sandbox for a hard guarantee) |
|
|
160
192
|
| Not giving the reviewer `Bash`/git access | Without it the reviewer can't run `git diff` — keep `bash`, or pass the diff in the prompt |
|
|
161
|
-
| Not
|
|
193
|
+
| Not catching `AgentExecutionError` | A failed reviewer run raises, it does not return |
|
|
162
194
|
| Reviewing with the same model that wrote the code | Use a different model or agent type for independence |
|
|
163
195
|
| Ignoring the review and committing anyway | At minimum, address all CRITICAL items first |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: invoking-cli-agents
|
|
3
|
-
description: Use when programmatically invoking a CLI coding agent (Claude Code, OpenCode, Copilot CLI, Codex, Pi) from Python, delegating a task to a sub-agent, orchestrating several agents, streaming an agent's output, resuming an agent session, restricting which tools an agent may use, or checking whether an agent/model is healthy. Keywords: AgentShell, headless agent, subprocess, allowed_tools, disallowed_tools, read-only agent, session_id, cost, output_tokens.
|
|
3
|
+
description: Use when programmatically invoking a CLI coding agent (Claude Code, OpenCode, Copilot CLI, Codex, Pi, Cursor) from Python, delegating a task to a sub-agent, orchestrating several agents, streaming an agent's output, resuming an agent session, restricting which tools an agent may use, or checking whether an agent/model is healthy. Keywords: AgentShell, headless agent, subprocess, allowed_tools, disallowed_tools, read-only agent, session_id, cost, output_tokens.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Invoking CLI Agents with AgentShell
|
|
@@ -15,7 +15,7 @@ passes `model` strings through verbatim and does not manage credentials.
|
|
|
15
15
|
|
|
16
16
|
## When to Use
|
|
17
17
|
|
|
18
|
-
- You need to invoke Claude Code, OpenCode, Copilot CLI, Codex, or
|
|
18
|
+
- You need to invoke Claude Code, OpenCode, Copilot CLI, Codex, Pi, or Cursor from Python
|
|
19
19
|
- You want to delegate a coding task to a sub-agent and collect the result
|
|
20
20
|
- You need to orchestrate multi-step workflows across agents
|
|
21
21
|
- You want to stream agent output in real-time
|
|
@@ -120,11 +120,11 @@ Both `execute()` and `stream()` take the same parameters.
|
|
|
120
120
|
|-----------|------|---------|---------|
|
|
121
121
|
| `cwd` | `str` | required | Working directory (must exist, else `ValueError`) |
|
|
122
122
|
| `prompt` | `str` | required | Task or question for the agent |
|
|
123
|
-
| `allowed_tools` | `list[str] \| None` | `None` | Whitelist of tools (agent-native names). `None` = all tools. Honoured by Claude Code, Copilot CLI, Pi; ignored by OpenCode and
|
|
123
|
+
| `allowed_tools` | `list[str] \| None` | `None` | Whitelist of tools (agent-native names). `None` = all tools. Honoured by Claude Code, Copilot CLI, Pi; ignored by OpenCode, Codex and Cursor. **Only actually enforced when `auto_approve=False`** (see Tool Restriction). |
|
|
124
124
|
| `disallowed_tools` | `list[str] \| None` | `None` | Denylist using a canonical vocabulary (see Tool Restriction). Deny takes precedence over allow **and** over `auto_approve`, but covers only built-in tools. Enforcement varies per agent; unenforceable denies emit a `UserWarning`. |
|
|
125
125
|
| `model` | `str \| None` | `None` | Model alias or name, passed to the CLI verbatim (e.g. `"sonnet"`, `"opencode/big-pickle"`) |
|
|
126
|
-
| `effort` | `str \| None` | `None` | Reasoning effort: `"low"`, `"medium"`, `"high"`, etc. Claude Code, Copilot, Codex, Pi. **Ignored by OpenCode
|
|
127
|
-
| `include_thinking` | `bool` | `False` | Yield `thinking` events in `stream()`. Claude Code, Copilot, Pi. **Dropped by `execute()`** (which keeps only text). |
|
|
126
|
+
| `effort` | `str \| None` | `None` | Reasoning effort: `"low"`, `"medium"`, `"high"`, etc. Claude Code, Copilot, Codex, Pi. **Ignored by OpenCode** (silently) **and Cursor** (warns). |
|
|
127
|
+
| `include_thinking` | `bool` | `False` | Yield `thinking` events in `stream()`. Claude Code, Copilot, Pi, Cursor. **Dropped by `execute()`** (which keeps only text). |
|
|
128
128
|
| `auto_approve` | `bool` | `True` | Skip tool permission prompts. Mapped on every adapter (Pi *requires* a trust decision — the default avoids a hang). **On Claude Code the default `True` sends `--dangerously-skip-permissions`, which bypasses `allowed_tools`.** |
|
|
129
129
|
| `session_id` | `str \| None` | `None` | Resume a previous session |
|
|
130
130
|
|
|
@@ -143,7 +143,7 @@ AgentType.OPENCODE # OpenCode CLI
|
|
|
143
143
|
AgentType.COPILOT_CLI # GitHub Copilot CLI
|
|
144
144
|
AgentType.CODEX # OpenAI Codex CLI
|
|
145
145
|
AgentType.PI # Pi coding agent
|
|
146
|
-
AgentType.
|
|
146
|
+
AgentType.CURSOR # Cursor CLI (cursor-agent)
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
Capabilities differ by agent. `output_tokens` is populated on all of them; the rest varies:
|
|
@@ -155,6 +155,7 @@ Capabilities differ by agent. `output_tokens` is populated on all of them; the r
|
|
|
155
155
|
| Copilot CLI | ✅ | ⚠️ `bash`, `edit` only | ✅ | ❌ `0.0` | ✅ real | ✅ |
|
|
156
156
|
| Codex | ❌ | ⚠️ `web_search` only | ✅ | ❌ `0.0` | ❌ `0.0` | ✅ |
|
|
157
157
|
| Pi | ✅ | ⚠️ `bash`, `edit`, `read` | ✅ | ⚠️ paid providers only | ❌ `0.0` | ❌ raises |
|
|
158
|
+
| Cursor | ❌ warns | ❌ none — warns | ❌ warns | ❌ `0.0` | ✅ real | ❌ raises |
|
|
158
159
|
|
|
159
160
|
A `✅` for `allowed_tools` means the flag is passed — but it only *enforces* with
|
|
160
161
|
`auto_approve=False`; `disallowed_tools` covers only built-in tools. See Tool Restriction.
|
|
@@ -188,7 +189,9 @@ response = await shell.execute(
|
|
|
188
189
|
auto_approve=False,
|
|
189
190
|
)
|
|
190
191
|
|
|
191
|
-
# OpenCode
|
|
192
|
+
# OpenCode ignores allowed_tools, but its denylist IS enforced — use that instead. (Codex and
|
|
193
|
+
# Cursor ignore BOTH controls: Codex can deny only web_search, Cursor nothing at all, so for
|
|
194
|
+
# those two in-library scoping buys you nothing — restrict them outside the library.)
|
|
192
195
|
# Keep auto_approve at its default True: on OpenCode, auto_approve=False makes `opencode run`
|
|
193
196
|
# auto-reject prompts and can silently abort the turn. (auto_approve=False is only for the
|
|
194
197
|
# whitelist path above, on Claude Code / Copilot / Pi.)
|
|
@@ -211,28 +214,41 @@ response = await shell.execute(
|
|
|
211
214
|
|
|
212
215
|
## Error Handling
|
|
213
216
|
|
|
214
|
-
AgentShell raises exceptions for its own preconditions
|
|
215
|
-
as events, not exceptions.**
|
|
217
|
+
AgentShell raises exceptions for its own preconditions **and** for a failed agent run:
|
|
216
218
|
|
|
217
219
|
```python
|
|
218
220
|
# cwd validation - raises ValueError if directory doesn't exist
|
|
219
|
-
# Unsupported agent
|
|
221
|
+
# Unsupported agent - an AgentType with no registered adapter raises ValueError
|
|
220
222
|
# KeyboardInterrupt - AgentShell cancels the subprocess cleanly, then re-raises
|
|
221
223
|
```
|
|
222
224
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
`
|
|
225
|
+
A run counts as failed when any of: an `error` event was emitted; the terminal `result` event
|
|
226
|
+
has `content == "error"`; or no terminal `result` event arrived at all (a killed, truncated, or
|
|
227
|
+
aborted run — OpenCode in particular can drop the tail and still exit 0). `execute()` raises
|
|
228
|
+
`AgentExecutionError` for all three; `str(e)` is the bare reason (e.g. `"500 model
|
|
229
|
+
name=qwen3.6-27b-8Q failed to load"`), and the exception also carries whatever partial data the
|
|
230
|
+
run produced before failing:
|
|
227
231
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
```python
|
|
233
|
+
from agent_shell.models.agent import AgentExecutionError
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
response = await shell.execute(cwd=project_path, prompt="do something")
|
|
237
|
+
except AgentExecutionError as e:
|
|
238
|
+
print(f"failed: {e}") # str(e) == e.reason, the bare cause
|
|
239
|
+
print(e.response) # text produced before the failure, if any
|
|
240
|
+
print(e.cost, e.session_id, e.duration, e.output_tokens)
|
|
241
|
+
else:
|
|
242
|
+
print(response.response)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`stream()` has no exception equivalent — it yields the raw events and leaves the verdict to the
|
|
246
|
+
caller. Apply the same rule as a **positive** signal, the way the library's own `health_check`
|
|
247
|
+
does: success is the LAST `result` event carrying `content == "ok"` **and** no `error` event —
|
|
248
|
+
the loop below already does this, since each `result` overwrites the verdict. (An agent that
|
|
249
|
+
retries a transient fault emits a failing `result` and then a good one.) Never infer
|
|
250
|
+
success from "got some text and no error event." Neither `execute()` nor `stream()` has a
|
|
251
|
+
timeout, so wrap the call in `asyncio.wait_for` to guard against a hang.
|
|
236
252
|
|
|
237
253
|
```python
|
|
238
254
|
saw_ok = False
|
|
@@ -254,8 +270,9 @@ succeeded = saw_ok and error is None # absent result => succeeded stays
|
|
|
254
270
|
event of every adapter. It reads `0` when the `result` event never arrives (a truncated turn),
|
|
255
271
|
so it is not a standalone liveness check — pair it with the success check above.
|
|
256
272
|
- `cost` — real for Claude Code and paid Pi providers; frequently `0.0` for OpenCode,
|
|
257
|
-
Copilot, and
|
|
258
|
-
|
|
273
|
+
Copilot, Codex, and always `0.0` for Cursor (they don't report it). Don't treat
|
|
274
|
+
`cost == 0` as "the call failed".
|
|
275
|
+
- `duration` — real for Claude Code, Copilot CLI, and Cursor; `0.0` elsewhere.
|
|
259
276
|
|
|
260
277
|
## Other Capabilities
|
|
261
278
|
|
|
@@ -264,7 +281,7 @@ succeeded = saw_ok and error is None # absent result => succeeded stays
|
|
|
264
281
|
**not** run your prompt, so use the stream-based check above when you care about a specific
|
|
265
282
|
call's outcome.
|
|
266
283
|
- **MCP server management** — `add_mcp_server`, `remove_mcp_server`, `list_mcp_servers` manage
|
|
267
|
-
the underlying CLI's MCP config (Pi
|
|
284
|
+
the underlying CLI's MCP config (Pi and Cursor raise `NotImplementedError`).
|
|
268
285
|
|
|
269
286
|
See [api-reference.md](api-reference.md) for their full signatures and the `MCPServerSpec` model.
|
|
270
287
|
|
|
@@ -286,10 +303,10 @@ logging.getLogger("agent_shell").addHandler(logging.StreamHandler())
|
|
|
286
303
|
| Stream events live | `async for event in shell.stream(cwd, prompt)` |
|
|
287
304
|
| Continue a conversation | Pass `session_id=response.session_id` |
|
|
288
305
|
| Whitelist tools (Claude/Copilot/Pi) | `allowed_tools=["Read", "Glob"]` |
|
|
289
|
-
| Deny tools
|
|
306
|
+
| Deny tools (Claude/OpenCode/Copilot/Pi) | `disallowed_tools=["edit", "bash"]` |
|
|
290
307
|
| Track usage | Read `response.output_tokens` (portable) or `response.cost` |
|
|
291
308
|
| Use a specific model | `model="sonnet"` |
|
|
292
|
-
| Increase reasoning depth | `effort="high"` (not OpenCode) |
|
|
309
|
+
| Increase reasoning depth | `effort="high"` (not OpenCode or Cursor) |
|
|
293
310
|
| See agent thinking | `include_thinking=True` in `stream()` |
|
|
294
311
|
| Check an agent/model works | `await shell.health_check(cwd, model=...)` |
|
|
295
312
|
| Cancel a running agent | `KeyboardInterrupt` (handled automatically) |
|
|
@@ -302,14 +319,14 @@ logging.getLogger("agent_shell").addHandler(logging.StreamHandler())
|
|
|
302
319
|
| Forgetting `await` | Both `execute()` and the `stream()` iterator are async |
|
|
303
320
|
| `allowed_tools=[]` to disable tools | Empty list is falsy → full access. Use a non-empty list or `disallowed_tools` |
|
|
304
321
|
| `allowed_tools` with the default `auto_approve=True` as a safety boundary | `--dangerously-skip-permissions` bypasses it. Set `auto_approve=False`, or use `disallowed_tools` |
|
|
305
|
-
| Relying on `allowed_tools` with OpenCode/Codex |
|
|
322
|
+
| Relying on `allowed_tools` with OpenCode/Codex/Cursor | Ignored; only OpenCode's deny works |
|
|
306
323
|
| Trusting a `prompt` instruction ("don't edit files") as a guarantee | Enforce it (`disallowed_tools`, or whitelist + `auto_approve=False`) |
|
|
307
324
|
| Assuming `disallowed_tools` sandboxes the agent | It covers only built-in tools; MCP/other-named tools bypass it. OS-sandbox for a hard guarantee |
|
|
308
325
|
| Treating `cost == 0` as failure | Many agents don't report cost; use `output_tokens` |
|
|
309
|
-
| Expecting `execute()` to expose thinking
|
|
326
|
+
| Expecting `execute()` to expose thinking | Use `stream()` with `include_thinking=True` |
|
|
327
|
+
| Not catching `AgentExecutionError` | `execute()` raises on a failed run — catch it |
|
|
310
328
|
| Ignoring `UserWarning` on a deny | An unenforceable deny is warned, not applied — the tool is NOT blocked |
|
|
311
329
|
| Ignoring `session_id` for multi-step work | Without it, each call starts fresh |
|
|
312
|
-
| Using `AgentType.GEMINI_CLI` | No adapter — raises `ValueError` at construction |
|
|
313
330
|
|
|
314
331
|
## API Reference
|
|
315
332
|
|
{agent_shell_py-0.1.17 → agent_shell_py-0.2.0}/agent-skills/invoking-cli-agents/api-reference.md
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# AgentShell API Reference
|
|
2
2
|
|
|
3
|
-
- [Models](#models) — `AgentType`, `AgentResponse`, `
|
|
3
|
+
- [Models](#models) — `AgentType`, `AgentResponse`, `AgentExecutionError`, `StreamEvent`,
|
|
4
|
+
`MCPServerSpec`, `HealthCheckResult`
|
|
4
5
|
- [StreamEvent types](#event-types)
|
|
5
6
|
- [AgentShell class](#agentshell-class) — `execute`, `stream`, `health_check`, MCP management
|
|
6
7
|
- [AgentAdapter protocol](#agentadapter-protocol)
|
|
@@ -16,15 +17,15 @@ from agent_shell.models.agent import AgentType
|
|
|
16
17
|
class AgentType(StrEnum):
|
|
17
18
|
CLAUDE_CODE = "claude_code"
|
|
18
19
|
OPENCODE = "opencode"
|
|
19
|
-
GEMINI_CLI = "gemini_cli" # enum only — NO adapter (raises ValueError)
|
|
20
20
|
COPILOT_CLI = "copilot_cli"
|
|
21
21
|
CODEX = "codex"
|
|
22
22
|
PI = "pi"
|
|
23
|
+
CURSOR = "cursor"
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
### AgentResponse
|
|
26
27
|
|
|
27
|
-
Returned by `execute()
|
|
28
|
+
Returned by `execute()` on success.
|
|
28
29
|
|
|
29
30
|
```python
|
|
30
31
|
@dataclass
|
|
@@ -36,6 +37,29 @@ class AgentResponse:
|
|
|
36
37
|
output_tokens: int = 0 # Generated tokens (reasoning-inclusive; populated on all agents)
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
### AgentExecutionError
|
|
41
|
+
|
|
42
|
+
Raised by `execute()` — never returned — when the run failed: an `error` event was emitted, the
|
|
43
|
+
terminal `result` event had `content == "error"`, or no terminal `result` event arrived at all
|
|
44
|
+
(a killed, truncated, or aborted run). `str(e)` is the bare reason (e.g. `"500 model
|
|
45
|
+
name=qwen3.6-27b-8Q failed to load"`). The constructor arguments double as attributes, carrying
|
|
46
|
+
whatever partial data the run produced before failing.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from agent_shell.models.agent import AgentExecutionError
|
|
50
|
+
|
|
51
|
+
class AgentExecutionError(Exception):
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
reason: str, # str(e) == reason; also e.reason
|
|
55
|
+
response: str = "", # text produced before the failure, if any
|
|
56
|
+
cost: float = 0.0,
|
|
57
|
+
session_id: str | None = None,
|
|
58
|
+
duration: float = 0.0,
|
|
59
|
+
output_tokens: int = 0,
|
|
60
|
+
): ...
|
|
61
|
+
```
|
|
62
|
+
|
|
39
63
|
### StreamEvent
|
|
40
64
|
|
|
41
65
|
Yielded by `stream()`.
|
|
@@ -49,6 +73,7 @@ class StreamEvent:
|
|
|
49
73
|
duration: float = 0.0 # Elapsed seconds (on "result" events, where supported)
|
|
50
74
|
session_id: str | None = None # On session-start and "result" events
|
|
51
75
|
output_tokens: int = 0 # Cumulative generated tokens (on "result" events)
|
|
76
|
+
error: str | None = None # Why a failing "result" failed, when recoverable (Pi)
|
|
52
77
|
```
|
|
53
78
|
|
|
54
79
|
### MCPServerSpec
|
|
@@ -91,14 +116,23 @@ Canonical event types emitted by `stream()`:
|
|
|
91
116
|
|------|------|---------|--------------|
|
|
92
117
|
| `system` | Session starts | `""` | `session_id` is set |
|
|
93
118
|
| `text` | Agent produces output | Response text (may arrive in chunks) | |
|
|
94
|
-
| `thinking` | Agent reasons (requires `include_thinking=True`; Claude Code / Copilot / Pi) | Chain-of-thought text | |
|
|
95
|
-
| `tool_use` | Agent invokes a tool | Tool name (Codex: the command
|
|
96
|
-
| `result` | Agent finishes | `"ok"`
|
|
97
|
-
| `error` | Agent or CLI process fails | Error message
|
|
119
|
+
| `thinking` | Agent reasons (requires `include_thinking=True`; Claude Code / Copilot / Pi / Cursor) | Chain-of-thought text | |
|
|
120
|
+
| `tool_use` | Agent invokes a tool | Tool name (Codex, and Cursor shell calls: the command) | |
|
|
121
|
+
| `result` | Agent finishes | `"ok"` or `"error"` | see the note below |
|
|
122
|
+
| `error` | Agent or CLI process fails | Error message, or stderr head+tail (500 chars each) | |
|
|
123
|
+
|
|
124
|
+
> A `result` event carries `cost`, `duration`, `output_tokens` and `session_id` on the agents
|
|
125
|
+
> that report them. On a failing result, `error` holds the reason when the adapter recovered a
|
|
126
|
+
> structured one (Pi); it is `None` otherwise.
|
|
98
127
|
|
|
99
128
|
> Codex emits the session-start event as `type="session"` (not `"system"`). If you branch on
|
|
100
129
|
> the session event across agents, match both.
|
|
101
130
|
|
|
131
|
+
> Long stderr is **not** tail-only. `format_stderr` keeps the first 500 *and* the last 500
|
|
132
|
+
> characters, joined by a `... [truncated] ...` marker, so a reason stated up front
|
|
133
|
+
> (cursor-agent's `Cannot use this model: <name>`) survives alongside a trailing stack trace.
|
|
134
|
+
> A stderr of 1000 characters or fewer is passed through whole, with no marker.
|
|
135
|
+
|
|
102
136
|
## AgentShell Class
|
|
103
137
|
|
|
104
138
|
```python
|
|
@@ -106,7 +140,7 @@ from agent_shell.shell import AgentShell
|
|
|
106
140
|
|
|
107
141
|
class AgentShell:
|
|
108
142
|
def __init__(self, agent_type: AgentType): ...
|
|
109
|
-
# raises ValueError for an AgentType with no adapter
|
|
143
|
+
# raises ValueError for an AgentType with no registered adapter
|
|
110
144
|
|
|
111
145
|
async def execute(
|
|
112
146
|
self,
|
|
@@ -120,13 +154,14 @@ class AgentShell:
|
|
|
120
154
|
session_id: str | None = None, # Resume previous session
|
|
121
155
|
disallowed_tools: list[str] | None = None, # Canonical denylist (enforced; deny > allow)
|
|
122
156
|
) -> AgentResponse: ...
|
|
157
|
+
# Raises AgentExecutionError if the run failed; see the Models section above.
|
|
123
158
|
|
|
124
159
|
def stream(self, ...) -> AsyncIterator[StreamEvent]: ... # same parameters as execute()
|
|
125
160
|
|
|
126
161
|
async def health_check(
|
|
127
162
|
self, cwd: str, model: str | None = None, timeout: float = 60.0,
|
|
128
163
|
) -> HealthCheckResult: ...
|
|
129
|
-
# Sends a trivial no-tool prompt; healthy iff
|
|
164
|
+
# Sends a trivial no-tool prompt; healthy iff the LAST result event is "ok" and no error.
|
|
130
165
|
|
|
131
166
|
async def add_mcp_server(self, mcp_server: MCPServerSpec) -> None: ...
|
|
132
167
|
async def remove_mcp_server(self, mcp_server_name: str) -> None: ...
|
|
@@ -156,6 +191,7 @@ class AgentAdapter(Protocol):
|
|
|
156
191
|
async def execute(self, cwd, prompt, allowed_tools=None, model=None, effort=None,
|
|
157
192
|
include_thinking=False, auto_approve=True, session_id=None,
|
|
158
193
|
disallowed_tools=None) -> AgentResponse: ...
|
|
194
|
+
# Raises AgentExecutionError if the run failed.
|
|
159
195
|
|
|
160
196
|
def stream(self, ...) -> AsyncIterator[StreamEvent]: ... # same signature as execute()
|
|
161
197
|
|
|
@@ -199,3 +235,24 @@ class AgentAdapter(Protocol):
|
|
|
199
235
|
- `effort` → `--thinking` (levels: off/minimal/low/medium/high/xhigh); `auto_approve` → `--approve` / `--no-approve` (one is always sent, else `pi -p` hangs on a trust prompt).
|
|
200
236
|
- `cost` is real for paid providers (`0.0` on local); `duration` is `0.0`.
|
|
201
237
|
- MCP-management methods raise `NotImplementedError`.
|
|
238
|
+
|
|
239
|
+
### Cursor
|
|
240
|
+
- `model` → `--model` (e.g. `"sonnet-4-thinking"`, `"gpt-5"`); parameterised models take bracket
|
|
241
|
+
overrides, e.g. `"claude-opus-4-8[context=1m,effort=high]"`. A Free plan exposes only `auto`.
|
|
242
|
+
- `allowed_tools` **and** `disallowed_tools` are both **ignored** (both warn). Cursor has no
|
|
243
|
+
per-call tool policy of any kind — it lives in `.cursor/cli.json` — so *nothing* in this library
|
|
244
|
+
can scope a Cursor agent; restrict it outside (read-only mount, container). `disallowed_tools`
|
|
245
|
+
warns on every call; `allowed_tools` and `effort` warn once per adapter instance.
|
|
246
|
+
- `effort` is **ignored** (warns): Cursor has no effort flag, only the model bracket-override
|
|
247
|
+
above, which the adapter does not inject. `include_thinking` **is** honoured — Cursor streams
|
|
248
|
+
reasoning as `thinking` deltas.
|
|
249
|
+
- `auto_approve` → `--force`; without it tools auto-*reject* but the run still completes (exit 0).
|
|
250
|
+
`--trust` is always sent regardless: untrusted, `cursor-agent` exits 1 with zero stdout and a
|
|
251
|
+
plain-text "Workspace Trust Required" on stderr.
|
|
252
|
+
- Session resume is `--resume=<id>` (the `=` form binds the id, whose CLI arg is optional). The
|
|
253
|
+
resumed run reports the SAME id — but an unknown id is **accepted**, creating a session under
|
|
254
|
+
it rather than failing (like Pi; unlike Claude Code, OpenCode, Copilot and Codex, which all
|
|
255
|
+
reject one). A matching id is therefore not proof a prior transcript was continued.
|
|
256
|
+
- `duration` and `output_tokens` are real (`usage.outputTokens`); `cost` is always `0.0` — Cursor
|
|
257
|
+
reports no cost. MCP-management methods raise `NotImplementedError` (`cursor-agent mcp` has no
|
|
258
|
+
add/remove, and its `list` returns only name+status, which cannot rebuild an `MCPServerSpec`).
|