agentshim 0.15.1__tar.gz → 0.15.3__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 (112) hide show
  1. {agentshim-0.15.1 → agentshim-0.15.3}/CHANGELOG.md +43 -0
  2. {agentshim-0.15.1 → agentshim-0.15.3}/PKG-INFO +1 -1
  3. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/__init__.py +19 -1
  4. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/__init__.py +21 -1
  5. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/conversation.py +22 -1
  6. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/errors.py +9 -0
  7. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/events.py +107 -1
  8. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/profile.py +13 -1
  9. agentshim-0.15.3/agentshim/core/status.py +110 -0
  10. agentshim-0.15.3/agentshim/probe.py +116 -0
  11. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/__init__.py +25 -0
  12. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/events.py +5 -1
  13. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/parser.py +12 -1
  14. agentshim-0.15.3/agentshim/providers/claude/probe.py +46 -0
  15. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/provider.py +1 -0
  16. agentshim-0.15.3/agentshim/providers/claude/rate_limits.py +81 -0
  17. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/stream_argv.py +3 -0
  18. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/stream_transport.py +176 -11
  19. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/profile.py +1 -0
  20. agentshim-0.15.3/agentshim/providers/codex/app_server/rate_limits.py +56 -0
  21. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/transport.py +91 -0
  22. agentshim-0.15.3/agentshim/providers/codex/probe.py +45 -0
  23. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/provider.py +1 -0
  24. agentshim-0.15.3/agentshim/providers/copilot/probe.py +10 -0
  25. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/copilot/provider.py +3 -0
  26. agentshim-0.15.3/agentshim/providers/gemini/probe.py +10 -0
  27. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/gemini/provider.py +1 -0
  28. agentshim-0.15.3/agentshim/providers/opencode/probe.py +10 -0
  29. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/opencode/provider.py +1 -0
  30. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/runtime.py +22 -0
  31. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/session.py +32 -1
  32. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/testing/__init__.py +35 -0
  33. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/testing/claude_stream.py +76 -6
  34. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/testing/codex_app_server.py +102 -2
  35. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/testing/contracts.py +94 -2
  36. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/testing/conversation.py +34 -4
  37. agentshim-0.15.3/agentshim/testing/probe.py +63 -0
  38. {agentshim-0.15.1 → agentshim-0.15.3}/pyproject.toml +1 -1
  39. {agentshim-0.15.1 → agentshim-0.15.3}/.gitignore +0 -0
  40. {agentshim-0.15.1 → agentshim-0.15.3}/LICENSE +0 -0
  41. {agentshim-0.15.1 → agentshim-0.15.3}/README.md +0 -0
  42. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/agent.py +0 -0
  43. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/_files.py +0 -0
  44. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/checkpoints.py +0 -0
  45. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/clock.py +0 -0
  46. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/env.py +0 -0
  47. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/home.py +0 -0
  48. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/ids.py +0 -0
  49. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/mcp.py +0 -0
  50. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/permissions.py +0 -0
  51. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/pricing.py +0 -0
  52. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/provider.py +0 -0
  53. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/schema.py +0 -0
  54. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/session_policy.py +0 -0
  55. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/skills.py +0 -0
  56. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/stream.py +0 -0
  57. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/turn.py +0 -0
  58. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/core/usage.py +0 -0
  59. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/execution/__init__.py +0 -0
  60. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/execution/confinement.py +0 -0
  61. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/execution/docker.py +0 -0
  62. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/execution/executor.py +0 -0
  63. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/execution/host.py +0 -0
  64. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/execution/process.py +0 -0
  65. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/execution/transform.py +0 -0
  66. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/oneshot.py +0 -0
  67. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/__init__.py +0 -0
  68. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/failures.py +0 -0
  69. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/hooks/__init__.py +0 -0
  70. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
  71. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/sandbox.py +0 -0
  72. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/scripted.py +0 -0
  73. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/claude/user_hooks.py +0 -0
  74. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/__init__.py +0 -0
  75. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/_toml.py +0 -0
  76. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/__init__.py +0 -0
  77. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/_wire.py +0 -0
  78. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/approvals.py +0 -0
  79. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/channel.py +0 -0
  80. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/errors.py +0 -0
  81. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/items.py +0 -0
  82. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/mcp.py +0 -0
  83. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/permissions.py +0 -0
  84. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/protocol.py +0 -0
  85. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/app_server/usage.py +0 -0
  86. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/events.py +0 -0
  87. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/failures.py +0 -0
  88. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/mcp_scope.py +0 -0
  89. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/parser.py +0 -0
  90. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/rules.py +0 -0
  91. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/sandbox.py +0 -0
  92. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/scripted.py +0 -0
  93. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/codex/skills.py +0 -0
  94. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/copilot/__init__.py +0 -0
  95. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/copilot/events.py +0 -0
  96. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/copilot/parser.py +0 -0
  97. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/copilot/scripted.py +0 -0
  98. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/gemini/__init__.py +0 -0
  99. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/gemini/events.py +0 -0
  100. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/gemini/parser.py +0 -0
  101. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/gemini/scripted.py +0 -0
  102. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/opencode/__init__.py +0 -0
  103. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/opencode/events.py +0 -0
  104. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/opencode/parser.py +0 -0
  105. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/providers/opencode/scripted.py +0 -0
  106. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/py.typed +0 -0
  107. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/testing/clock.py +0 -0
  108. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/testing/confinement.py +0 -0
  109. {agentshim-0.15.1 → agentshim-0.15.3}/agentshim/testing/process.py +0 -0
  110. {agentshim-0.15.1 → agentshim-0.15.3}/tests/fixtures/claude_stream/README.md +0 -0
  111. {agentshim-0.15.1 → agentshim-0.15.3}/tests/fixtures/codex/README.md +0 -0
  112. {agentshim-0.15.1 → agentshim-0.15.3}/tests/fixtures/codex_app_server/README.md +0 -0
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.3 (2026-10-09)
4
+
5
+ ### Added
6
+
7
+ - `ProviderProfile.credential_files` (default empty): the subset of `auth_files`
8
+ that holds refreshable login credentials, as opposed to settings. A caller that
9
+ lends a login to another process shares these files writable so a token
10
+ refresh there reaches the original login. Claude `.claude/.credentials.json`,
11
+ Codex `.codex/auth.json`, Gemini `.gemini/oauth_creds.json`, opencode
12
+ `.local/share/opencode/auth.json`, Copilot `.copilot/config.json`.
13
+
14
+ ## 0.15.2 (2026-10-09)
15
+
16
+ ### Added
17
+
18
+ - `RateLimitStatus` event (also in `AgentEvent` and `ConsoleEventHandler`):
19
+ the provider's report of one rate-limit window, with `window`, `limit`,
20
+ `used_fraction`, `remaining_fraction`, `resets_at`, `window_minutes`,
21
+ `exhausted` and `raw`. Unstated values are `None`, never zero. Emitted by the
22
+ Claude stream transport and one-shot parser (`rate_limit_event`) and by the
23
+ Codex app-server transport (`account/rateLimits/updated`). Test doubles:
24
+ `ClaudePeerTurn(rate_limit=...)` and the `ReportRateLimits` Codex script step.
25
+ - Provider readiness probe. `probe_provider(name, *, executor, confinement, env,
26
+ timeout)` and `Agent.probe()` return a `ProviderStatus` (`binary_found`,
27
+ `path`, `version`, `auth: AuthState`, `auth_detail`) without running a turn or
28
+ calling a model, through the executor and confinement a turn would use. Claude
29
+ uses `claude auth status`, Codex `codex login status`; Gemini, Copilot and
30
+ opencode have no status command and report `AuthState.UNKNOWN`. A missing
31
+ binary is a result, not an error. Test double `agentshim.testing.probe_executor`.
32
+ - Mid-turn steering. `Session.steer(text)` (thread-safe) sends a message into the
33
+ running turn; `SteerableConversation` is the transport-tier protocol and
34
+ `ProviderProfile.supports_steer` the capability (true for the Claude and Codex
35
+ stream transports). It never queues: `ProviderCapabilityError` when
36
+ unsupported, new `NoRunningTurnError` (a `SessionStateError`) when no turn can
37
+ take it. Events `SteerDelivered`, `SteerConsumed`, `SteerRejected`.
38
+ - Codex maps to `turn/steer`. Claude Code writes a user envelope and runs with
39
+ `--replay-user-messages`; `turn()` still returns one `TurnResult` when the CLI
40
+ queues the message as a follow-on turn (new `steer_grace_s` option).
41
+ - Test doubles: `AwaitSteer` and `CodexScript(steer_refusal=)`,
42
+ `ClaudePeerTurn(injects_steer=)` and `ClaudeStreamPeers(drops_queued_messages=)`,
43
+ `fake_profile(supports_steer=)`, `FakeConversation.steer`, and
44
+ `SteerableConversationContract`.
45
+
3
46
  ## 0.15.1 (2026-10-09)
4
47
 
5
48
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentshim
3
- Version: 0.15.1
3
+ Version: 0.15.3
4
4
  Summary: Provider-agnostic coding-agent CLI shims
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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.1"
159
+ __version__ = "0.15.3"
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 Conversation, ConversationSpec, Transport, TransportKind
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))
@@ -152,7 +152,9 @@ class ProviderProfile:
152
152
  ``state_root_env`` is the documented variable that relocates
153
153
  ``state_dirs[0]``, or ``None`` when the CLI names none. ``auth_files`` are
154
154
  the home-relative paths, each inside a ``state_dirs`` entry, whose
155
- contents are the minimal set to copy so the CLI is logged in elsewhere.
155
+ contents are the minimal set to copy so the CLI is logged in elsewhere;
156
+ ``credential_files`` (0.15.3+, default empty) is the subset of them that
157
+ holds refreshable login credentials rather than settings.
156
158
  ``mcp_config_file`` is the workspace-relative path a ``CONFIG_FILE``
157
159
  provider writes its MCP config to for a turn, and ``None`` for
158
160
  ``CLI_FLAGS``/``NONE`` providers.
@@ -174,6 +176,12 @@ class ProviderProfile:
174
176
  container_env: Mapping[str, str] = field(default_factory=_no_container_env)
175
177
  state_root_env: str | None = None
176
178
  auth_files: tuple[str, ...] = ()
179
+ #: The subset of ``auth_files`` that holds login credentials (OAuth tokens
180
+ #: the CLI itself refreshes and rewrites), as opposed to settings. A caller
181
+ #: that lends a login to another process shares exactly these files
182
+ #: writable so a token refresh there is not lost, and keeps the rest of
183
+ #: ``auth_files`` read-only. Every entry is also in ``auth_files``.
184
+ credential_files: tuple[str, ...] = ()
177
185
  mcp_config_file: str | None = None
178
186
  #: Whether the stream lists the skills offered (``SkillsDiscovered``).
179
187
  skill_discovery: SkillSignal = SkillSignal.NONE
@@ -198,3 +206,7 @@ class ProviderProfile:
198
206
  #: When a session should retire a conversation of this provider; ``None``
199
207
  #: means never.
200
208
  renewal: RenewalBudget | None = None
209
+ #: Whether a conversation on this provider takes ``steer(text)`` while a
210
+ #: turn runs. A provider that does not (every one-shot transport) rejects
211
+ #: it with ``ProviderCapabilityError`` instead of delaying the message.
212
+ 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