agentshim 0.15.0__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.
Files changed (112) hide show
  1. {agentshim-0.15.0 → agentshim-0.15.2}/CHANGELOG.md +66 -0
  2. {agentshim-0.15.0 → agentshim-0.15.2}/PKG-INFO +1 -1
  3. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/__init__.py +25 -2
  4. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/__init__.py +22 -1
  5. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/conversation.py +33 -1
  6. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/errors.py +9 -0
  7. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/events.py +107 -1
  8. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/profile.py +4 -0
  9. agentshim-0.15.2/agentshim/core/status.py +110 -0
  10. agentshim-0.15.2/agentshim/probe.py +116 -0
  11. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/__init__.py +78 -1
  12. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/__init__.py +3 -0
  13. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/events.py +23 -1
  14. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/parser.py +26 -5
  15. agentshim-0.15.2/agentshim/providers/claude/probe.py +46 -0
  16. agentshim-0.15.2/agentshim/providers/claude/rate_limits.py +81 -0
  17. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/sandbox.py +31 -2
  18. agentshim-0.15.2/agentshim/providers/claude/stream_argv.py +150 -0
  19. agentshim-0.15.2/agentshim/providers/claude/stream_transport.py +963 -0
  20. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/app_server/__init__.py +6 -2
  21. agentshim-0.15.2/agentshim/providers/codex/app_server/approvals.py +152 -0
  22. agentshim-0.15.2/agentshim/providers/codex/app_server/channel.py +260 -0
  23. agentshim-0.15.2/agentshim/providers/codex/app_server/errors.py +74 -0
  24. agentshim-0.15.2/agentshim/providers/codex/app_server/items.py +173 -0
  25. agentshim-0.15.2/agentshim/providers/codex/app_server/mcp.py +75 -0
  26. agentshim-0.15.2/agentshim/providers/codex/app_server/permissions.py +105 -0
  27. agentshim-0.15.2/agentshim/providers/codex/app_server/profile.py +25 -0
  28. agentshim-0.15.2/agentshim/providers/codex/app_server/rate_limits.py +56 -0
  29. agentshim-0.15.2/agentshim/providers/codex/app_server/transport.py +1009 -0
  30. agentshim-0.15.2/agentshim/providers/codex/app_server/usage.py +100 -0
  31. agentshim-0.15.2/agentshim/providers/codex/probe.py +45 -0
  32. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/provider.py +29 -10
  33. agentshim-0.15.2/agentshim/providers/copilot/probe.py +10 -0
  34. agentshim-0.15.2/agentshim/providers/gemini/probe.py +10 -0
  35. agentshim-0.15.2/agentshim/providers/opencode/probe.py +10 -0
  36. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/runtime.py +55 -8
  37. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/session.py +32 -1
  38. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/testing/__init__.py +85 -0
  39. agentshim-0.15.2/agentshim/testing/claude_stream.py +616 -0
  40. agentshim-0.15.2/agentshim/testing/codex_app_server.py +1127 -0
  41. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/testing/contracts.py +94 -2
  42. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/testing/conversation.py +34 -4
  43. agentshim-0.15.2/agentshim/testing/probe.py +63 -0
  44. {agentshim-0.15.0 → agentshim-0.15.2}/pyproject.toml +1 -1
  45. agentshim-0.15.2/tests/fixtures/claude_stream/README.md +18 -0
  46. {agentshim-0.15.0 → agentshim-0.15.2}/.gitignore +0 -0
  47. {agentshim-0.15.0 → agentshim-0.15.2}/LICENSE +0 -0
  48. {agentshim-0.15.0 → agentshim-0.15.2}/README.md +0 -0
  49. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/agent.py +0 -0
  50. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/_files.py +0 -0
  51. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/checkpoints.py +0 -0
  52. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/clock.py +0 -0
  53. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/env.py +0 -0
  54. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/home.py +0 -0
  55. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/ids.py +0 -0
  56. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/mcp.py +0 -0
  57. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/permissions.py +0 -0
  58. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/pricing.py +0 -0
  59. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/provider.py +0 -0
  60. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/schema.py +0 -0
  61. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/session_policy.py +0 -0
  62. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/skills.py +0 -0
  63. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/stream.py +0 -0
  64. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/turn.py +0 -0
  65. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/core/usage.py +0 -0
  66. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/execution/__init__.py +0 -0
  67. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/execution/confinement.py +0 -0
  68. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/execution/docker.py +0 -0
  69. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/execution/executor.py +0 -0
  70. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/execution/host.py +0 -0
  71. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/execution/process.py +0 -0
  72. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/execution/transform.py +0 -0
  73. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/oneshot.py +0 -0
  74. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/failures.py +0 -0
  75. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/hooks/__init__.py +0 -0
  76. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
  77. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/provider.py +0 -0
  78. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/scripted.py +0 -0
  79. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/claude/user_hooks.py +0 -0
  80. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/__init__.py +0 -0
  81. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/_toml.py +0 -0
  82. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/app_server/_wire.py +0 -0
  83. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/app_server/protocol.py +0 -0
  84. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/events.py +0 -0
  85. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/failures.py +0 -0
  86. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/mcp_scope.py +0 -0
  87. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/parser.py +0 -0
  88. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/rules.py +0 -0
  89. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/sandbox.py +0 -0
  90. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/scripted.py +0 -0
  91. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/codex/skills.py +0 -0
  92. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/copilot/__init__.py +0 -0
  93. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/copilot/events.py +0 -0
  94. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/copilot/parser.py +0 -0
  95. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/copilot/provider.py +0 -0
  96. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/copilot/scripted.py +0 -0
  97. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/gemini/__init__.py +0 -0
  98. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/gemini/events.py +0 -0
  99. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/gemini/parser.py +0 -0
  100. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/gemini/provider.py +0 -0
  101. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/gemini/scripted.py +0 -0
  102. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/opencode/__init__.py +0 -0
  103. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/opencode/events.py +0 -0
  104. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/opencode/parser.py +0 -0
  105. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/opencode/provider.py +0 -0
  106. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/providers/opencode/scripted.py +0 -0
  107. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/py.typed +0 -0
  108. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/testing/clock.py +0 -0
  109. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/testing/confinement.py +0 -0
  110. {agentshim-0.15.0 → agentshim-0.15.2}/agentshim/testing/process.py +0 -0
  111. {agentshim-0.15.0 → agentshim-0.15.2}/tests/fixtures/codex/README.md +0 -0
  112. {agentshim-0.15.0 → agentshim-0.15.2}/tests/fixtures/codex_app_server/README.md +0 -0
@@ -1,5 +1,49 @@
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
+
35
+ ## 0.15.1 (2026-10-09)
36
+
37
+ ### Added
38
+
39
+ - `CodexAppServerTransport`: a long-lived `codex app-server` per conversation,
40
+ registered as Codex's stream transport, so
41
+ `Agent("codex", transport=TransportKind.STREAM)` selects it (one-shot stays
42
+ the default). Supports read-only, workspace-write and bypass
43
+ permissions, always-answered server requests, per-turn usage diffing, failure
44
+ classification, interrupt and timeout. Test double `CodexScript` /
45
+ `CodexAppServerPeer` in `agentshim.testing`.
46
+
3
47
  ## 0.15.0 (2026-10-09)
4
48
 
5
49
  ### Added
@@ -31,6 +75,28 @@
31
75
  `ConversationContract`, `CheckpointStoreContract`.
32
76
  - Docs: [sessions](https://vic-lsh.github.io/agentshim/sessions/).
33
77
 
78
+ ### Added (Claude stream transport)
79
+
80
+ - `ClaudeStreamTransport`: Claude Code over one long-lived
81
+ `claude --input-format stream-json --output-format stream-json` process per
82
+ conversation (no `claude-agent-sdk`). `Agent("claude", transport=TransportKind.STREAM,
83
+ ...)` selects it; `TransportKind.ONE_SHOT` stays the default for now.
84
+ Supports `NativeMode.BYPASS` and `NativeMode.WORKSPACE_WRITE` (Claude's bash
85
+ sandbox plus a write-confinement hook for its file tools, no network);
86
+ `READ_ONLY` and network access are rejected with `ProviderCapabilityError`.
87
+ Permission prompts are denied by the CLI (`--permission-prompts none`) and any
88
+ request that arrives anyway is answered at once per `ApprovalPolicy`. A turn
89
+ whose output schema, MCP servers, effort or extra arguments differ from the
90
+ running process restarts it with `--resume` (the conversation id is unchanged).
91
+ - `TransportKind`, `agentshim.providers.get_stream_transport`.
92
+ - Test doubles `ClaudeStreamPeers` / `ClaudeStreamPeer` (a reactive fake CLI
93
+ scripted with `ClaudePeerTurn`, `ClaudeApiError`, `ClaudeCrash`) and
94
+ `ClaudeRecordedPeer` (replays recorded frames), plus recordings under
95
+ `tests/fixtures/claude_stream`.
96
+ - `ClaudeStreamParser(..., cost_baseline_usd=)` and `.total_cost_usd` for
97
+ per-turn cost on a process that reports cumulative totals; `ResultFrame`
98
+ gains `terminal_reason` and `permission_denials`.
99
+
34
100
  ### Added (process layer)
35
101
 
36
102
  - MIT license (`LICENSE`), declared in the package metadata.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentshim
3
- Version: 0.15.0
3
+ Version: 0.15.2
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,
@@ -93,6 +101,7 @@ from .core import (
93
101
  ToolResult,
94
102
  ToolTracker,
95
103
  Transport,
104
+ TransportKind,
96
105
  TurnCancelledError,
97
106
  TurnFailedError,
98
107
  TurnInterrupted,
@@ -136,16 +145,18 @@ from .execution import (
136
145
  confine,
137
146
  )
138
147
  from .oneshot import OneShotTransport
148
+ from .probe import probe_provider
139
149
  from .providers import get_provider, provider_names
140
- from .providers.claude import ClaudeHook, ClaudeProvider, SandboxConfig
150
+ from .providers.claude import ClaudeHook, ClaudeProvider, ClaudeStreamTransport, SandboxConfig
141
151
  from .providers.codex import CodexProvider, CodexSandboxConfig
152
+ from .providers.codex.app_server import CodexAppServerTransport
142
153
  from .providers.copilot import CopilotProvider
143
154
  from .providers.gemini import GeminiProvider
144
155
  from .providers.opencode import OpencodeProvider
145
156
  from .runtime import Agent
146
157
  from .session import Session, Turn, TurnTicket, map_mcp_servers
147
158
 
148
- __version__ = "0.15.0"
159
+ __version__ = "0.15.2"
149
160
 
150
161
  __all__ = [
151
162
  "Agent",
@@ -157,17 +168,20 @@ __all__ = [
157
168
  "ApprovalPolicy",
158
169
  "ArgvContext",
159
170
  "AssistantText",
171
+ "AuthState",
160
172
  "CallbackCommandStreamSink",
161
173
  "Checkpoint",
162
174
  "CheckpointStore",
163
175
  "ClaudeHook",
164
176
  "ClaudeProvider",
177
+ "ClaudeStreamTransport",
165
178
  "CliAgent",
166
179
  "CliCheckError",
167
180
  "CliExitError",
168
181
  "CliNotFoundError",
169
182
  "CliTimeoutError",
170
183
  "Clock",
184
+ "CodexAppServerTransport",
171
185
  "CodexProvider",
172
186
  "CodexSandboxConfig",
173
187
  "CommandExecutor",
@@ -206,6 +220,7 @@ __all__ = [
206
220
  "ModelPricing",
207
221
  "NativeMode",
208
222
  "NativePermissions",
223
+ "NoRunningTurnError",
209
224
  "NoopInstallation",
210
225
  "NullEventHandler",
211
226
  "NullSink",
@@ -225,8 +240,10 @@ __all__ = [
225
240
  "ProviderCapabilityError",
226
241
  "ProviderError",
227
242
  "ProviderProfile",
243
+ "ProviderStatus",
228
244
  "ProviderUsage",
229
245
  "RandomIds",
246
+ "RateLimitStatus",
230
247
  "RawOutput",
231
248
  "ReapError",
232
249
  "Reasoning",
@@ -254,6 +271,10 @@ __all__ = [
254
271
  "StderrLine",
255
272
  "StdioMcpServer",
256
273
  "StdoutLine",
274
+ "SteerConsumed",
275
+ "SteerDelivered",
276
+ "SteerRejected",
277
+ "SteerableConversation",
257
278
  "StopSignal",
258
279
  "StreamParser",
259
280
  "SystemClock",
@@ -264,6 +285,7 @@ __all__ = [
264
285
  "ToolTracker",
265
286
  "TransformingExecutor",
266
287
  "Transport",
288
+ "TransportKind",
267
289
  "Turn",
268
290
  "TurnCancelledError",
269
291
  "TurnFailedError",
@@ -290,5 +312,6 @@ __all__ = [
290
312
  "parse_json_object",
291
313
  "prepare_config_home",
292
314
  "price_for",
315
+ "probe_provider",
293
316
  "provider_names",
294
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
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",
@@ -182,6 +202,7 @@ __all__ = [
182
202
  "ToolResult",
183
203
  "ToolTracker",
184
204
  "Transport",
205
+ "TransportKind",
185
206
  "TurnCancelledError",
186
207
  "TurnFailedError",
187
208
  "TurnInterrupted",
@@ -10,7 +10,8 @@ nothing else changes.
10
10
  from __future__ import annotations
11
11
 
12
12
  from dataclasses import dataclass
13
- from typing import TYPE_CHECKING, Protocol
13
+ from enum import Enum
14
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
14
15
 
15
16
  from .profile import ConfigScope, McpScope, SkillScope
16
17
 
@@ -25,6 +26,16 @@ if TYPE_CHECKING:
25
26
  from .usage import ProviderUsage
26
27
 
27
28
 
29
+ class TransportKind(Enum):
30
+ """Which way an ``Agent`` reaches a provider by name."""
31
+
32
+ #: One CLI process per turn (today's behaviour, the default for now).
33
+ ONE_SHOT = "one_shot"
34
+ #: One long-lived process per conversation, over the provider's structured
35
+ #: streaming protocol.
36
+ STREAM = "stream"
37
+
38
+
28
39
  @dataclass(frozen=True)
29
40
  class ConversationSpec:
30
41
  """Everything fixed for the life of one conversation.
@@ -85,6 +96,27 @@ class Conversation(Protocol):
85
96
  ...
86
97
 
87
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
+
88
120
  class Transport(Protocol):
89
121
  """A way to reach one provider, producing conversations."""
90
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