agentshim 0.15.1__tar.gz → 0.15.2__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.15.1 → agentshim-0.15.2}/CHANGELOG.md +32 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/PKG-INFO +1 -1
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/__init__.py +19 -1
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/__init__.py +21 -1
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/conversation.py +22 -1
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/errors.py +9 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/events.py +107 -1
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/profile.py +4 -0
- agentshim-0.15.2/agentshim/core/status.py +110 -0
- agentshim-0.15.2/agentshim/probe.py +116 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/__init__.py +25 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/events.py +5 -1
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/parser.py +12 -1
- agentshim-0.15.2/agentshim/providers/claude/probe.py +46 -0
- agentshim-0.15.2/agentshim/providers/claude/rate_limits.py +81 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/stream_argv.py +3 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/stream_transport.py +176 -11
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/profile.py +1 -0
- agentshim-0.15.2/agentshim/providers/codex/app_server/rate_limits.py +56 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/transport.py +91 -0
- agentshim-0.15.2/agentshim/providers/codex/probe.py +45 -0
- agentshim-0.15.2/agentshim/providers/copilot/probe.py +10 -0
- agentshim-0.15.2/agentshim/providers/gemini/probe.py +10 -0
- agentshim-0.15.2/agentshim/providers/opencode/probe.py +10 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/runtime.py +22 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/session.py +32 -1
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/testing/__init__.py +35 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/testing/claude_stream.py +76 -6
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/testing/codex_app_server.py +102 -2
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/testing/contracts.py +94 -2
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/testing/conversation.py +34 -4
- agentshim-0.15.2/agentshim/testing/probe.py +63 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/pyproject.toml +1 -1
- {agentshim-0.15.1 → agentshim-0.15.2}/.gitignore +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/LICENSE +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/README.md +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/agent.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/_files.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/checkpoints.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/clock.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/env.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/home.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/ids.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/mcp.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/permissions.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/pricing.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/provider.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/schema.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/session_policy.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/skills.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/stream.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/turn.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/core/usage.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/execution/confinement.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/execution/docker.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/execution/executor.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/execution/host.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/execution/process.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/execution/transform.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/oneshot.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/failures.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/provider.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/__init__.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/_wire.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/approvals.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/channel.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/errors.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/items.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/mcp.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/permissions.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/protocol.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/app_server/usage.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/events.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/failures.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/mcp_scope.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/parser.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/provider.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/scripted.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/py.typed +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/testing/clock.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/testing/confinement.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/agentshim/testing/process.py +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/tests/fixtures/claude_stream/README.md +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/tests/fixtures/codex/README.md +0 -0
- {agentshim-0.15.1 → agentshim-0.15.2}/tests/fixtures/codex_app_server/README.md +0 -0
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.2 (2026-10-09)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `RateLimitStatus` event (also in `AgentEvent` and `ConsoleEventHandler`):
|
|
8
|
+
the provider's report of one rate-limit window, with `window`, `limit`,
|
|
9
|
+
`used_fraction`, `remaining_fraction`, `resets_at`, `window_minutes`,
|
|
10
|
+
`exhausted` and `raw`. Unstated values are `None`, never zero. Emitted by the
|
|
11
|
+
Claude stream transport and one-shot parser (`rate_limit_event`) and by the
|
|
12
|
+
Codex app-server transport (`account/rateLimits/updated`). Test doubles:
|
|
13
|
+
`ClaudePeerTurn(rate_limit=...)` and the `ReportRateLimits` Codex script step.
|
|
14
|
+
- Provider readiness probe. `probe_provider(name, *, executor, confinement, env,
|
|
15
|
+
timeout)` and `Agent.probe()` return a `ProviderStatus` (`binary_found`,
|
|
16
|
+
`path`, `version`, `auth: AuthState`, `auth_detail`) without running a turn or
|
|
17
|
+
calling a model, through the executor and confinement a turn would use. Claude
|
|
18
|
+
uses `claude auth status`, Codex `codex login status`; Gemini, Copilot and
|
|
19
|
+
opencode have no status command and report `AuthState.UNKNOWN`. A missing
|
|
20
|
+
binary is a result, not an error. Test double `agentshim.testing.probe_executor`.
|
|
21
|
+
- Mid-turn steering. `Session.steer(text)` (thread-safe) sends a message into the
|
|
22
|
+
running turn; `SteerableConversation` is the transport-tier protocol and
|
|
23
|
+
`ProviderProfile.supports_steer` the capability (true for the Claude and Codex
|
|
24
|
+
stream transports). It never queues: `ProviderCapabilityError` when
|
|
25
|
+
unsupported, new `NoRunningTurnError` (a `SessionStateError`) when no turn can
|
|
26
|
+
take it. Events `SteerDelivered`, `SteerConsumed`, `SteerRejected`.
|
|
27
|
+
- Codex maps to `turn/steer`. Claude Code writes a user envelope and runs with
|
|
28
|
+
`--replay-user-messages`; `turn()` still returns one `TurnResult` when the CLI
|
|
29
|
+
queues the message as a follow-on turn (new `steer_grace_s` option).
|
|
30
|
+
- Test doubles: `AwaitSteer` and `CodexScript(steer_refusal=)`,
|
|
31
|
+
`ClaudePeerTurn(injects_steer=)` and `ClaudeStreamPeers(drops_queued_messages=)`,
|
|
32
|
+
`fake_profile(supports_steer=)`, `FakeConversation.steer`, and
|
|
33
|
+
`SteerableConversationContract`.
|
|
34
|
+
|
|
3
35
|
## 0.15.1 (2026-10-09)
|
|
4
36
|
|
|
5
37
|
### Added
|
|
@@ -15,6 +15,7 @@ from .core import (
|
|
|
15
15
|
ApprovalPolicy,
|
|
16
16
|
ArgvContext,
|
|
17
17
|
AssistantText,
|
|
18
|
+
AuthState,
|
|
18
19
|
Checkpoint,
|
|
19
20
|
CheckpointStore,
|
|
20
21
|
CliCheckError,
|
|
@@ -48,6 +49,7 @@ from .core import (
|
|
|
48
49
|
NativeMode,
|
|
49
50
|
NativePermissions,
|
|
50
51
|
NoopInstallation,
|
|
52
|
+
NoRunningTurnError,
|
|
51
53
|
NullEventHandler,
|
|
52
54
|
OutputSchema,
|
|
53
55
|
OutputSchemaStyle,
|
|
@@ -60,8 +62,10 @@ from .core import (
|
|
|
60
62
|
ProviderCapabilityError,
|
|
61
63
|
ProviderError,
|
|
62
64
|
ProviderProfile,
|
|
65
|
+
ProviderStatus,
|
|
63
66
|
ProviderUsage,
|
|
64
67
|
RandomIds,
|
|
68
|
+
RateLimitStatus,
|
|
65
69
|
RawOutput,
|
|
66
70
|
ReapError,
|
|
67
71
|
Reasoning,
|
|
@@ -84,6 +88,10 @@ from .core import (
|
|
|
84
88
|
SkillTracker,
|
|
85
89
|
Stderr,
|
|
86
90
|
StdioMcpServer,
|
|
91
|
+
SteerableConversation,
|
|
92
|
+
SteerConsumed,
|
|
93
|
+
SteerDelivered,
|
|
94
|
+
SteerRejected,
|
|
87
95
|
StopSignal,
|
|
88
96
|
StreamParser,
|
|
89
97
|
SystemClock,
|
|
@@ -137,6 +145,7 @@ from .execution import (
|
|
|
137
145
|
confine,
|
|
138
146
|
)
|
|
139
147
|
from .oneshot import OneShotTransport
|
|
148
|
+
from .probe import probe_provider
|
|
140
149
|
from .providers import get_provider, provider_names
|
|
141
150
|
from .providers.claude import ClaudeHook, ClaudeProvider, ClaudeStreamTransport, SandboxConfig
|
|
142
151
|
from .providers.codex import CodexProvider, CodexSandboxConfig
|
|
@@ -147,7 +156,7 @@ from .providers.opencode import OpencodeProvider
|
|
|
147
156
|
from .runtime import Agent
|
|
148
157
|
from .session import Session, Turn, TurnTicket, map_mcp_servers
|
|
149
158
|
|
|
150
|
-
__version__ = "0.15.
|
|
159
|
+
__version__ = "0.15.2"
|
|
151
160
|
|
|
152
161
|
__all__ = [
|
|
153
162
|
"Agent",
|
|
@@ -159,6 +168,7 @@ __all__ = [
|
|
|
159
168
|
"ApprovalPolicy",
|
|
160
169
|
"ArgvContext",
|
|
161
170
|
"AssistantText",
|
|
171
|
+
"AuthState",
|
|
162
172
|
"CallbackCommandStreamSink",
|
|
163
173
|
"Checkpoint",
|
|
164
174
|
"CheckpointStore",
|
|
@@ -210,6 +220,7 @@ __all__ = [
|
|
|
210
220
|
"ModelPricing",
|
|
211
221
|
"NativeMode",
|
|
212
222
|
"NativePermissions",
|
|
223
|
+
"NoRunningTurnError",
|
|
213
224
|
"NoopInstallation",
|
|
214
225
|
"NullEventHandler",
|
|
215
226
|
"NullSink",
|
|
@@ -229,8 +240,10 @@ __all__ = [
|
|
|
229
240
|
"ProviderCapabilityError",
|
|
230
241
|
"ProviderError",
|
|
231
242
|
"ProviderProfile",
|
|
243
|
+
"ProviderStatus",
|
|
232
244
|
"ProviderUsage",
|
|
233
245
|
"RandomIds",
|
|
246
|
+
"RateLimitStatus",
|
|
234
247
|
"RawOutput",
|
|
235
248
|
"ReapError",
|
|
236
249
|
"Reasoning",
|
|
@@ -258,6 +271,10 @@ __all__ = [
|
|
|
258
271
|
"StderrLine",
|
|
259
272
|
"StdioMcpServer",
|
|
260
273
|
"StdoutLine",
|
|
274
|
+
"SteerConsumed",
|
|
275
|
+
"SteerDelivered",
|
|
276
|
+
"SteerRejected",
|
|
277
|
+
"SteerableConversation",
|
|
261
278
|
"StopSignal",
|
|
262
279
|
"StreamParser",
|
|
263
280
|
"SystemClock",
|
|
@@ -295,5 +312,6 @@ __all__ = [
|
|
|
295
312
|
"parse_json_object",
|
|
296
313
|
"prepare_config_home",
|
|
297
314
|
"price_for",
|
|
315
|
+
"probe_provider",
|
|
298
316
|
"provider_names",
|
|
299
317
|
]
|
|
@@ -9,7 +9,13 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from .checkpoints import Checkpoint, CheckpointStore, InMemoryCheckpointStore
|
|
11
11
|
from .clock import Clock, StopSignal, SystemClock
|
|
12
|
-
from .conversation import
|
|
12
|
+
from .conversation import (
|
|
13
|
+
Conversation,
|
|
14
|
+
ConversationSpec,
|
|
15
|
+
SteerableConversation,
|
|
16
|
+
Transport,
|
|
17
|
+
TransportKind,
|
|
18
|
+
)
|
|
13
19
|
from .env import interactive_env
|
|
14
20
|
from .errors import (
|
|
15
21
|
AgentShimError,
|
|
@@ -20,6 +26,7 @@ from .errors import (
|
|
|
20
26
|
ContinuityError,
|
|
21
27
|
FailureKind,
|
|
22
28
|
McpConfigError,
|
|
29
|
+
NoRunningTurnError,
|
|
23
30
|
ProcessClosedError,
|
|
24
31
|
ProviderCapabilityError,
|
|
25
32
|
ReapError,
|
|
@@ -41,6 +48,7 @@ from .events import (
|
|
|
41
48
|
Lifecycle,
|
|
42
49
|
NullEventHandler,
|
|
43
50
|
ProviderError,
|
|
51
|
+
RateLimitStatus,
|
|
44
52
|
RawOutput,
|
|
45
53
|
Reasoning,
|
|
46
54
|
RunFinished,
|
|
@@ -49,6 +57,9 @@ from .events import (
|
|
|
49
57
|
SkillInvoked,
|
|
50
58
|
SkillsDiscovered,
|
|
51
59
|
Stderr,
|
|
60
|
+
SteerConsumed,
|
|
61
|
+
SteerDelivered,
|
|
62
|
+
SteerRejected,
|
|
52
63
|
ToolCall,
|
|
53
64
|
ToolResult,
|
|
54
65
|
TurnInterrupted,
|
|
@@ -92,6 +103,7 @@ from .provider import (
|
|
|
92
103
|
from .schema import compact_json, dialect_problems, materialize, normalize
|
|
93
104
|
from .session_policy import Continuity, PolicyConfig, RetryPolicy, SessionPolicy, SessionState
|
|
94
105
|
from .skills import SkillSummary, SkillTracker
|
|
106
|
+
from .status import AuthState, ProviderStatus
|
|
95
107
|
from .stream import ToolTracker, parse_json_object
|
|
96
108
|
from .turn import OutputSchema, TurnRequest, TurnResult
|
|
97
109
|
from .usage import ProviderUsage, TokenUsage, TokenWeights, normalized_usage
|
|
@@ -104,6 +116,7 @@ __all__ = [
|
|
|
104
116
|
"ApprovalPolicy",
|
|
105
117
|
"ArgvContext",
|
|
106
118
|
"AssistantText",
|
|
119
|
+
"AuthState",
|
|
107
120
|
"Checkpoint",
|
|
108
121
|
"CheckpointStore",
|
|
109
122
|
"CliCheckError",
|
|
@@ -136,6 +149,7 @@ __all__ = [
|
|
|
136
149
|
"ModelPricing",
|
|
137
150
|
"NativeMode",
|
|
138
151
|
"NativePermissions",
|
|
152
|
+
"NoRunningTurnError",
|
|
139
153
|
"NoopInstallation",
|
|
140
154
|
"NullEventHandler",
|
|
141
155
|
"OutputSchema",
|
|
@@ -149,8 +163,10 @@ __all__ = [
|
|
|
149
163
|
"ProviderCapabilityError",
|
|
150
164
|
"ProviderError",
|
|
151
165
|
"ProviderProfile",
|
|
166
|
+
"ProviderStatus",
|
|
152
167
|
"ProviderUsage",
|
|
153
168
|
"RandomIds",
|
|
169
|
+
"RateLimitStatus",
|
|
154
170
|
"RawOutput",
|
|
155
171
|
"ReapError",
|
|
156
172
|
"Reasoning",
|
|
@@ -173,6 +189,10 @@ __all__ = [
|
|
|
173
189
|
"SkillsDiscovered",
|
|
174
190
|
"Stderr",
|
|
175
191
|
"StdioMcpServer",
|
|
192
|
+
"SteerConsumed",
|
|
193
|
+
"SteerDelivered",
|
|
194
|
+
"SteerRejected",
|
|
195
|
+
"SteerableConversation",
|
|
176
196
|
"StopSignal",
|
|
177
197
|
"StreamParser",
|
|
178
198
|
"SystemClock",
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from enum import Enum
|
|
14
|
-
from typing import TYPE_CHECKING, Protocol
|
|
14
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
15
15
|
|
|
16
16
|
from .profile import ConfigScope, McpScope, SkillScope
|
|
17
17
|
|
|
@@ -96,6 +96,27 @@ class Conversation(Protocol):
|
|
|
96
96
|
...
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
@runtime_checkable
|
|
100
|
+
class SteerableConversation(Conversation, Protocol):
|
|
101
|
+
"""A conversation that accepts a message while a turn runs.
|
|
102
|
+
|
|
103
|
+
A transport whose profile has ``supports_steer`` opens conversations of
|
|
104
|
+
this kind. ``steer`` neither queues nor retries: the message goes to the
|
|
105
|
+
running turn now or the call raises.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def steer(self, text: str) -> None:
|
|
109
|
+
"""Send *text* into the running turn. Thread-safe.
|
|
110
|
+
|
|
111
|
+
Raises ``NoRunningTurnError`` when no turn is running or the running
|
|
112
|
+
one cannot take a message yet (or any longer). Returns once the message
|
|
113
|
+
is on its way; the outcome is reported as ``SteerDelivered``,
|
|
114
|
+
``SteerConsumed`` and ``SteerRejected`` events, on the thread running
|
|
115
|
+
the turn.
|
|
116
|
+
"""
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
|
|
99
120
|
class Transport(Protocol):
|
|
100
121
|
"""A way to reach one provider, producing conversations."""
|
|
101
122
|
|
|
@@ -227,6 +227,15 @@ class SessionStateError(AgentShimError):
|
|
|
227
227
|
"""
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
class NoRunningTurnError(SessionStateError):
|
|
231
|
+
"""``steer`` was called when no turn could take a message.
|
|
232
|
+
|
|
233
|
+
Either no turn is running, or the running one has not started yet or is
|
|
234
|
+
already finishing. agentshim never holds a steer back for a later turn: a
|
|
235
|
+
caller that wants one delivered later sends it with the next prompt.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
230
239
|
class TurnCancelledError(AgentShimError):
|
|
231
240
|
"""An interrupt arrived before the turn could start, so it never ran."""
|
|
232
241
|
|
|
@@ -8,7 +8,7 @@ the session sees it.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import sys
|
|
11
|
-
from dataclasses import dataclass
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
12
|
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
@@ -156,6 +156,83 @@ class ApprovalDenied:
|
|
|
156
156
|
detail: str
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
def _no_raw() -> Mapping[str, Any]:
|
|
160
|
+
return {}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class RateLimitStatus:
|
|
165
|
+
"""The provider's view of one rate-limit window, as it just reported it.
|
|
166
|
+
|
|
167
|
+
Optional: a provider that has no such signal emits nothing, and a field the
|
|
168
|
+
provider did not state is ``None``, never zero. One event describes one
|
|
169
|
+
window, so a provider that reports several (a five-hour and a weekly one)
|
|
170
|
+
emits several events. A consumer reads the newest event per
|
|
171
|
+
``(limit, window)``.
|
|
172
|
+
|
|
173
|
+
``window`` names the window in the provider's words (``five_hour``,
|
|
174
|
+
``primary``). ``limit`` names the limit it belongs to when the provider
|
|
175
|
+
has more than one (Codex ``limit_id``). ``used_fraction`` is the share of
|
|
176
|
+
the window already used, ``0.0`` to ``1.0`` (it may exceed 1 when the
|
|
177
|
+
provider reports over-use). ``resets_at`` is epoch seconds.
|
|
178
|
+
``exhausted`` is ``True`` when the provider says this limit is reached,
|
|
179
|
+
``False`` when it says it is not, ``None`` when it does not say.
|
|
180
|
+
``raw`` is the provider's own mapping, for diagnostics.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
window: str | None
|
|
184
|
+
used_fraction: float | None
|
|
185
|
+
resets_at: float | None
|
|
186
|
+
limit: str | None = None
|
|
187
|
+
window_minutes: int | None = None
|
|
188
|
+
exhausted: bool | None = None
|
|
189
|
+
raw: Mapping[str, Any] = field(default_factory=_no_raw)
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def remaining_fraction(self) -> float | None:
|
|
193
|
+
"""The share of the window still available, or ``None`` when usage is unknown."""
|
|
194
|
+
if self.used_fraction is None:
|
|
195
|
+
return None
|
|
196
|
+
return max(0.0, 1.0 - self.used_fraction)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True)
|
|
200
|
+
class SteerDelivered:
|
|
201
|
+
"""A message sent with ``steer`` reached the provider, which accepted it.
|
|
202
|
+
|
|
203
|
+
This says the provider holds the message, not that the model has seen it
|
|
204
|
+
(see ``SteerConsumed``). Emitted on the thread running the turn.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
text: str
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True)
|
|
211
|
+
class SteerConsumed:
|
|
212
|
+
"""The provider took a steered message into the running turn's context.
|
|
213
|
+
|
|
214
|
+
Emitted where the provider reports it: Codex when the message becomes an
|
|
215
|
+
item of the turn, Claude Code when it echoes the message back. A message
|
|
216
|
+
that the provider only queued for the next turn of its own (Claude Code,
|
|
217
|
+
when the model is not at a tool boundary) is consumed when that turn starts;
|
|
218
|
+
the steering turn then continues until that follow-on finishes.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
text: str
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True)
|
|
225
|
+
class SteerRejected:
|
|
226
|
+
"""The provider refused a steered message; the turn is unaffected.
|
|
227
|
+
|
|
228
|
+
``reason`` is the provider's own explanation. Codex refuses a steer for a
|
|
229
|
+
turn that cannot take one (a compaction, say) or that has just ended.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
text: str
|
|
233
|
+
reason: str
|
|
234
|
+
|
|
235
|
+
|
|
159
236
|
AgentEvent = (
|
|
160
237
|
RunStarted
|
|
161
238
|
| RunFinished
|
|
@@ -173,6 +250,10 @@ AgentEvent = (
|
|
|
173
250
|
| ProviderError
|
|
174
251
|
| TurnInterrupted
|
|
175
252
|
| ApprovalDenied
|
|
253
|
+
| RateLimitStatus
|
|
254
|
+
| SteerDelivered
|
|
255
|
+
| SteerConsumed
|
|
256
|
+
| SteerRejected
|
|
176
257
|
)
|
|
177
258
|
|
|
178
259
|
|
|
@@ -260,6 +341,19 @@ def _truncate_lines(content: str, max_lines: int = _TOOL_OUTPUT_MAX_LINES) -> st
|
|
|
260
341
|
return content
|
|
261
342
|
|
|
262
343
|
|
|
344
|
+
def _describe_rate_limit(event: RateLimitStatus) -> str:
|
|
345
|
+
"""One console line for a rate-limit window; unknown parts are left out."""
|
|
346
|
+
name = " ".join(part for part in (event.limit, event.window) if part) or "rate limit"
|
|
347
|
+
parts = [f"[rate limit] {name}"]
|
|
348
|
+
if event.used_fraction is not None:
|
|
349
|
+
parts.append(f"{event.used_fraction:.0%} used")
|
|
350
|
+
if event.exhausted:
|
|
351
|
+
parts.append("reached")
|
|
352
|
+
if event.resets_at is not None:
|
|
353
|
+
parts.append(f"resets at epoch {event.resets_at:.0f}")
|
|
354
|
+
return ", ".join(parts)
|
|
355
|
+
|
|
356
|
+
|
|
263
357
|
class ConsoleEventHandler:
|
|
264
358
|
"""Render events as human-readable lines on a text stream.
|
|
265
359
|
|
|
@@ -392,6 +486,14 @@ class ConsoleEventHandler:
|
|
|
392
486
|
else:
|
|
393
487
|
self._line(self._paint(f"{event.tool} ran successfully", colour))
|
|
394
488
|
|
|
489
|
+
def _render_steer(self, event: SteerDelivered | SteerConsumed | SteerRejected) -> None:
|
|
490
|
+
if isinstance(event, SteerRejected):
|
|
491
|
+
self._line(f"[steer rejected] {event.text}: {event.reason}")
|
|
492
|
+
elif isinstance(event, SteerConsumed):
|
|
493
|
+
self._line(self._paint(f"[steer consumed] {event.text}", self._DIM))
|
|
494
|
+
else:
|
|
495
|
+
self._line(self._paint(f"[steer delivered] {event.text}", self._DIM))
|
|
496
|
+
|
|
395
497
|
def _render_diagnostic(self, event: AgentEvent) -> None:
|
|
396
498
|
"""Write the tagged non-output events, and drop the rest.
|
|
397
499
|
|
|
@@ -408,5 +510,9 @@ class ConsoleEventHandler:
|
|
|
408
510
|
self._line(self._paint("[interrupted]", self._DIM))
|
|
409
511
|
elif isinstance(event, ApprovalDenied):
|
|
410
512
|
self._line(f"[approval denied] {event.kind}: {event.detail}")
|
|
513
|
+
elif isinstance(event, RateLimitStatus):
|
|
514
|
+
self._line(self._paint(_describe_rate_limit(event), self._DIM))
|
|
515
|
+
elif isinstance(event, (SteerDelivered, SteerConsumed, SteerRejected)):
|
|
516
|
+
self._render_steer(event)
|
|
411
517
|
elif isinstance(event, Lifecycle) and self._show_lifecycle:
|
|
412
518
|
self._line(self._paint(f"[{event.kind}] {event.detail}", self._DIM))
|
|
@@ -198,3 +198,7 @@ class ProviderProfile:
|
|
|
198
198
|
#: When a session should retire a conversation of this provider; ``None``
|
|
199
199
|
#: means never.
|
|
200
200
|
renewal: RenewalBudget | None = None
|
|
201
|
+
#: Whether a conversation on this provider takes ``steer(text)`` while a
|
|
202
|
+
#: turn runs. A provider that does not (every one-shot transport) rejects
|
|
203
|
+
#: it with ``ProviderCapabilityError`` instead of delaying the message.
|
|
204
|
+
supports_steer: bool = False
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Provider readiness: what a probe reports, and how a provider declares its checks.
|
|
2
|
+
|
|
3
|
+
A probe answers "could a turn start?" without running one: is the binary
|
|
4
|
+
there, which version is it, is it logged in. It never calls a model. The
|
|
5
|
+
answer is a ``ProviderStatus``; each provider declares its own checks as a
|
|
6
|
+
``ProbeSpec``, and ``agentshim.probe`` runs them through an executor.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Callable, Mapping
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuthState(str, Enum):
|
|
21
|
+
"""Whether the provider CLI is authenticated, as far as its own tooling can say.
|
|
22
|
+
|
|
23
|
+
``UNKNOWN`` is distinct from ``FAILED``: it means the CLI offers no cheap
|
|
24
|
+
way to tell (or the check itself could not run), so a caller must not
|
|
25
|
+
treat it as a problem.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
KNOWN_OK = "known_ok"
|
|
29
|
+
FAILED = "failed"
|
|
30
|
+
UNKNOWN = "unknown"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ProviderStatus:
|
|
35
|
+
"""The readiness of one provider CLI on one executor.
|
|
36
|
+
|
|
37
|
+
``version`` is ``None`` when the binary is missing or did not report one.
|
|
38
|
+
``auth_detail`` explains ``auth`` in words fit for a log or an error
|
|
39
|
+
message, including the fix when authentication failed.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
provider: str
|
|
43
|
+
binary_found: bool
|
|
44
|
+
path: str | None
|
|
45
|
+
version: str | None
|
|
46
|
+
auth: AuthState
|
|
47
|
+
auth_detail: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class ProbeOutput:
|
|
52
|
+
"""What a status command printed."""
|
|
53
|
+
|
|
54
|
+
returncode: int
|
|
55
|
+
stdout: str
|
|
56
|
+
stderr: str
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def text(self) -> str:
|
|
60
|
+
"""Both streams, stdout first."""
|
|
61
|
+
return self.stdout + self.stderr
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class AuthCheck:
|
|
66
|
+
"""A provider's reading of its status command's output."""
|
|
67
|
+
|
|
68
|
+
state: AuthState
|
|
69
|
+
detail: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class AuthProbe:
|
|
74
|
+
"""A cheap, non-model command that reports authentication, and how to read it.
|
|
75
|
+
|
|
76
|
+
``argv`` follows the binary. ``read`` receives the output and the
|
|
77
|
+
environment the command ran with (an API key there changes what a failed
|
|
78
|
+
login means) and must return ``UNKNOWN`` for output it does not recognise
|
|
79
|
+
rather than guess.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
argv: tuple[str, ...]
|
|
83
|
+
read: Callable[[ProbeOutput, Mapping[str, str]], AuthCheck]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ProbeSpec:
|
|
88
|
+
"""A provider's readiness checks.
|
|
89
|
+
|
|
90
|
+
``auth`` is ``None`` for a CLI with no status mechanism; ``auth_note`` then
|
|
91
|
+
says why the state is unknown.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
auth: AuthProbe | None
|
|
95
|
+
auth_note: str = "this CLI has no authentication status command"
|
|
96
|
+
version_argv: tuple[str, ...] = ("--version",)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
_VERSION = re.compile(r"\d+(?:\.\d+)+(?:-[0-9A-Za-z.]+)?")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_version(text: str) -> str | None:
|
|
103
|
+
"""The first dotted version number in *text*, or ``None``.
|
|
104
|
+
|
|
105
|
+
``--version`` output varies (``2.1.295 (Claude Code)``,
|
|
106
|
+
``codex-cli 0.160.0``, ``GitHub Copilot CLI 1.0.94.``); the number is the
|
|
107
|
+
only part every CLI shares.
|
|
108
|
+
"""
|
|
109
|
+
match = _VERSION.search(text)
|
|
110
|
+
return match.group(0) if match else None
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Readiness probe: is a provider CLI installed, which version, logged in?
|
|
2
|
+
|
|
3
|
+
Composition layer, next to ``runtime``: it turns a provider name into the
|
|
4
|
+
provider's declared checks and runs them on an executor. Everything runs
|
|
5
|
+
through the executor and environment a turn would use, so a confinement or a
|
|
6
|
+
container is probed where the agent would run. No check calls a model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from agentshim.core.env import interactive_env
|
|
14
|
+
from agentshim.core.errors import AgentShimError, CliNotFoundError
|
|
15
|
+
from agentshim.core.status import (
|
|
16
|
+
AuthCheck,
|
|
17
|
+
AuthState,
|
|
18
|
+
ProbeOutput,
|
|
19
|
+
ProviderStatus,
|
|
20
|
+
parse_version,
|
|
21
|
+
)
|
|
22
|
+
from agentshim.execution.confinement import confine
|
|
23
|
+
from agentshim.execution.executor import CommandRequest, NullSink
|
|
24
|
+
from agentshim.execution.host import HostCommandExecutor
|
|
25
|
+
from agentshim.providers import get_probe_spec, get_provider
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Mapping, Sequence
|
|
29
|
+
|
|
30
|
+
from agentshim.execution.confinement import Confinement
|
|
31
|
+
from agentshim.execution.executor import CommandExecutor
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def probe_provider(
|
|
35
|
+
provider: str,
|
|
36
|
+
*,
|
|
37
|
+
executor: CommandExecutor | None = None,
|
|
38
|
+
confinement: Confinement | None = None,
|
|
39
|
+
env: Mapping[str, str] | None = None,
|
|
40
|
+
timeout: float = 15.0,
|
|
41
|
+
) -> ProviderStatus:
|
|
42
|
+
"""Report the readiness of *provider* without running a turn.
|
|
43
|
+
|
|
44
|
+
Arguments mean what they mean for ``Agent``: *executor* defaults to the
|
|
45
|
+
local host, *confinement* wraps it and supplies the environment (so *env*
|
|
46
|
+
may not also be given), and *env* otherwise defaults to the login shell's
|
|
47
|
+
environment. *timeout* bounds each of the two commands (``--version`` and
|
|
48
|
+
the provider's status command).
|
|
49
|
+
|
|
50
|
+
A missing binary is a result (``binary_found=False``), not an error, so a
|
|
51
|
+
startup check can report every provider in one pass. A command that cannot
|
|
52
|
+
run or times out leaves that field unknown; it never raises for a CLI
|
|
53
|
+
problem. An unknown *provider* raises ``ValueError``.
|
|
54
|
+
"""
|
|
55
|
+
base = executor if executor is not None else HostCommandExecutor()
|
|
56
|
+
if confinement is not None:
|
|
57
|
+
if env is not None:
|
|
58
|
+
msg = "env conflicts with confinement: the confinement supplies the agent environment"
|
|
59
|
+
raise ValueError(msg)
|
|
60
|
+
base = confine(base, confinement)
|
|
61
|
+
env = dict(confinement.env)
|
|
62
|
+
return probe_on(provider, base, dict(env) if env is not None else interactive_env(), timeout)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def probe_on(
|
|
66
|
+
provider: str, executor: CommandExecutor, env: Mapping[str, str], timeout: float
|
|
67
|
+
) -> ProviderStatus:
|
|
68
|
+
"""Probe *provider* on an executor that is already confined, with the final *env*."""
|
|
69
|
+
profile = get_provider(provider).profile
|
|
70
|
+
spec = get_probe_spec(provider)
|
|
71
|
+
try:
|
|
72
|
+
path = executor.find_binary(profile.binary, env)
|
|
73
|
+
except CliNotFoundError:
|
|
74
|
+
detail = f"{profile.binary} was not found on PATH"
|
|
75
|
+
return ProviderStatus(
|
|
76
|
+
provider=provider,
|
|
77
|
+
binary_found=False,
|
|
78
|
+
path=None,
|
|
79
|
+
version=None,
|
|
80
|
+
auth=AuthState.UNKNOWN,
|
|
81
|
+
auth_detail=detail,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
version_out = _run(executor, [path, *spec.version_argv], env, timeout)
|
|
85
|
+
version = None if version_out is None else parse_version(version_out.text)
|
|
86
|
+
if spec.auth is None:
|
|
87
|
+
check = AuthCheck(AuthState.UNKNOWN, spec.auth_note)
|
|
88
|
+
else:
|
|
89
|
+
auth_out = _run(executor, [path, *spec.auth.argv], env, timeout)
|
|
90
|
+
if auth_out is None:
|
|
91
|
+
check = AuthCheck(
|
|
92
|
+
AuthState.UNKNOWN,
|
|
93
|
+
f"`{profile.binary} {' '.join(spec.auth.argv)}` did not run or timed out",
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
check = spec.auth.read(auth_out, env)
|
|
97
|
+
return ProviderStatus(
|
|
98
|
+
provider=provider,
|
|
99
|
+
binary_found=True,
|
|
100
|
+
path=path,
|
|
101
|
+
version=version,
|
|
102
|
+
auth=check.state,
|
|
103
|
+
auth_detail=check.detail,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _run(
|
|
108
|
+
executor: CommandExecutor, argv: Sequence[str], env: Mapping[str, str], timeout: float
|
|
109
|
+
) -> ProbeOutput | None:
|
|
110
|
+
"""Run one status command; ``None`` when it could not run or timed out."""
|
|
111
|
+
request = CommandRequest(argv=argv, stdin=None, cwd=None, env=env, timeout=timeout)
|
|
112
|
+
try:
|
|
113
|
+
result = executor.run(request, NullSink())
|
|
114
|
+
except (AgentShimError, OSError):
|
|
115
|
+
return None
|
|
116
|
+
return ProbeOutput(result.returncode, result.stdout, result.stderr)
|