agentshim 0.13.1__tar.gz → 0.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {agentshim-0.13.1 → agentshim-0.14.0}/CHANGELOG.md +24 -0
  2. {agentshim-0.13.1 → agentshim-0.14.0}/PKG-INFO +1 -1
  3. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/__init__.py +5 -1
  4. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/agent.py +40 -5
  5. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/__init__.py +11 -1
  6. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/provider.py +21 -2
  7. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/usage.py +6 -0
  8. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/events.py +1 -1
  9. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/parser.py +47 -15
  10. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/provider.py +4 -1
  11. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/scripted.py +2 -1
  12. {agentshim-0.13.1 → agentshim-0.14.0}/pyproject.toml +1 -1
  13. agentshim-0.14.0/tests/fixtures/codex/README.md +18 -0
  14. {agentshim-0.13.1 → agentshim-0.14.0}/.gitignore +0 -0
  15. {agentshim-0.13.1 → agentshim-0.14.0}/README.md +0 -0
  16. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/_files.py +0 -0
  17. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/env.py +0 -0
  18. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/errors.py +0 -0
  19. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/events.py +0 -0
  20. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/home.py +0 -0
  21. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/mcp.py +0 -0
  22. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/pricing.py +0 -0
  23. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/profile.py +0 -0
  24. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/schema.py +0 -0
  25. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/skills.py +0 -0
  26. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/stream.py +0 -0
  27. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/turn.py +0 -0
  28. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/execution/__init__.py +0 -0
  29. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/execution/executor.py +0 -0
  30. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/execution/host.py +0 -0
  31. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/execution/transform.py +0 -0
  32. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/__init__.py +0 -0
  33. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/__init__.py +0 -0
  34. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/events.py +0 -0
  35. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/failures.py +0 -0
  36. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
  37. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
  38. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/parser.py +0 -0
  39. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/provider.py +0 -0
  40. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/sandbox.py +0 -0
  41. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/scripted.py +0 -0
  42. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/user_hooks.py +0 -0
  43. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/__init__.py +0 -0
  44. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/_toml.py +0 -0
  45. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/failures.py +0 -0
  46. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/mcp_scope.py +0 -0
  47. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/rules.py +0 -0
  48. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/sandbox.py +0 -0
  49. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/skills.py +0 -0
  50. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/__init__.py +0 -0
  51. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/events.py +0 -0
  52. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/parser.py +0 -0
  53. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/provider.py +0 -0
  54. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/scripted.py +0 -0
  55. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/__init__.py +0 -0
  56. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/events.py +0 -0
  57. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/parser.py +0 -0
  58. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/provider.py +0 -0
  59. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/scripted.py +0 -0
  60. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/__init__.py +0 -0
  61. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/events.py +0 -0
  62. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/parser.py +0 -0
  63. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/provider.py +0 -0
  64. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/scripted.py +0 -0
  65. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/py.typed +0 -0
  66. {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/testing/__init__.py +0 -0
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0 (2026-10-04)
4
+
5
+ Codex resumed turns report invocation usage. Additive.
6
+
7
+ ### Added
8
+
9
+ - `ProviderUsage.increment_known` marks whether the invocation's token
10
+ increment is measurable. Unknown increments retain raw cumulative usage
11
+ and zero token placeholders; consumers must check the marker before
12
+ pricing or budgeting.
13
+ - `previous_usage` on session creation and adoption seeds a checkpointed
14
+ conversation's usage baseline. `ParserContext` and `ContextualStreamParser`
15
+ let providers receive invocation context without changing provider factories.
16
+
17
+ ### Fixed
18
+
19
+ - Codex `turn.completed` usage is cumulative per thread. Resumed turns now
20
+ subtract the prior raw usage report for every token class instead of
21
+ counting earlier tokens again. Repeated completion snapshots replace the
22
+ previous snapshot, and conversations retain independent baselines,
23
+ including after failed invocations.
24
+ - Package `__version__` and its test now match the project metadata; the
25
+ 0.13.1 release left both at 0.13.0.
26
+
3
27
  ## 0.13.1 (2026-10-03)
4
28
 
5
29
  Fix only.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentshim
3
- Version: 0.13.1
3
+ Version: 0.14.0
4
4
  Summary: Provider-agnostic coding-agent CLI shims
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: tomli>=2; python_full_version < '3.11'
@@ -21,6 +21,7 @@ from .core import (
21
21
  ConfigFileInstallation,
22
22
  ConfigScope,
23
23
  ConsoleEventHandler,
24
+ ContextualStreamParser,
24
25
  EventHandlerBase,
25
26
  FailureKind,
26
27
  FlagsInstallation,
@@ -38,6 +39,7 @@ from .core import (
38
39
  OutputSchema,
39
40
  OutputSchemaStyle,
40
41
  ParsedTurn,
42
+ ParserContext,
41
43
  PricingTable,
42
44
  Provider,
43
45
  ProviderCapabilityError,
@@ -101,7 +103,7 @@ from .providers.copilot import CopilotProvider
101
103
  from .providers.gemini import GeminiProvider
102
104
  from .providers.opencode import OpencodeProvider
103
105
 
104
- __version__ = "0.13.0"
106
+ __version__ = "0.14.0"
105
107
 
106
108
  __all__ = [
107
109
  "AgentEvent",
@@ -129,6 +131,7 @@ __all__ = [
129
131
  "ConfigFileInstallation",
130
132
  "ConfigScope",
131
133
  "ConsoleEventHandler",
134
+ "ContextualStreamParser",
132
135
  "CopilotProvider",
133
136
  "EventHandlerBase",
134
137
  "FailureKind",
@@ -151,6 +154,7 @@ __all__ = [
151
154
  "OutputSchema",
152
155
  "OutputSchemaStyle",
153
156
  "ParsedTurn",
157
+ "ParserContext",
154
158
  "PricingTable",
155
159
  "Provider",
156
160
  "ProviderCapabilityError",
@@ -31,7 +31,7 @@ from agentshim.core.profile import (
31
31
  SchemaDialect,
32
32
  SkillScope,
33
33
  )
34
- from agentshim.core.provider import ArgvContext
34
+ from agentshim.core.provider import ArgvContext, ContextualStreamParser, ParserContext
35
35
  from agentshim.core.schema import compact_json, dialect_problems, materialize
36
36
  from agentshim.core.skills import SkillTracker
37
37
  from agentshim.core.turn import TurnRequest, TurnResult, coerce_request
@@ -46,6 +46,7 @@ if TYPE_CHECKING:
46
46
  from agentshim.core.profile import ProviderProfile
47
47
  from agentshim.core.provider import McpInstallation, ParsedTurn, Provider, StreamParser
48
48
  from agentshim.core.turn import OutputSchema
49
+ from agentshim.core.usage import ProviderUsage
49
50
  from agentshim.execution.executor import CommandExecutor, CommandHandle, CommandResult
50
51
 
51
52
 
@@ -121,6 +122,7 @@ class CliAgent:
121
122
  cwd: str | None = None,
122
123
  timeout: float | None = None,
123
124
  session_id: str | None = None,
125
+ previous_usage: ProviderUsage | None = None,
124
126
  skill_scope: SkillScope = SkillScope.ALL,
125
127
  mcp_scope: McpScope = McpScope.ALL,
126
128
  config_scope: ConfigScope = ConfigScope.ALL,
@@ -128,13 +130,16 @@ class CliAgent:
128
130
  """Open a conversation whose turns resume one another.
129
131
 
130
132
  ``skill_scope``, ``mcp_scope`` and ``config_scope`` are fixed for the
131
- conversation (see ``AgentSession``).
133
+ conversation (see ``AgentSession``). ``previous_usage`` seeds the
134
+ baseline when adopting a cumulative-usage provider conversation after
135
+ a restart; pass its most recent ``TurnResult.usage``.
132
136
  """
133
137
  return AgentSession(
134
138
  self,
135
139
  cwd=cwd,
136
140
  timeout=timeout,
137
141
  session_id=session_id,
142
+ previous_usage=previous_usage,
138
143
  skill_scope=skill_scope,
139
144
  mcp_scope=mcp_scope,
140
145
  config_scope=config_scope,
@@ -175,6 +180,7 @@ class AgentSession:
175
180
  cwd: str | None = None,
176
181
  timeout: float | None = None,
177
182
  session_id: str | None = None,
183
+ previous_usage: ProviderUsage | None = None,
178
184
  skill_scope: SkillScope = SkillScope.ALL,
179
185
  mcp_scope: McpScope = McpScope.ALL,
180
186
  config_scope: ConfigScope = ConfigScope.ALL,
@@ -185,7 +191,9 @@ class AgentSession:
185
191
  successive turns resume one another. ``cwd`` and ``timeout`` are
186
192
  defaults an individual ``TurnRequest`` may override. Passing
187
193
  ``session_id`` adopts a conversation the provider already has, so the
188
- first turn resumes rather than starts fresh. ``skill_scope`` limits
194
+ first turn resumes rather than starts fresh. ``previous_usage`` is
195
+ that conversation's last report, including raw cumulative totals,
196
+ when it was run outside this session. ``skill_scope`` limits
189
197
  which skills every turn's CLI may discover; a scope the provider's
190
198
  ``profile.skill_scopes`` does not list raises
191
199
  ``ProviderCapabilityError`` here, before any turn runs. ``mcp_scope``
@@ -215,6 +223,9 @@ class AgentSession:
215
223
  self._timeout = timeout
216
224
  self.session_id: str | None = session_id
217
225
  self.last_result: TurnResult | None = None
226
+ self._usage_by_session: dict[str, ProviderUsage] = {}
227
+ if session_id is not None and previous_usage is not None:
228
+ self._usage_by_session[session_id] = previous_usage
218
229
  self._lock = threading.Lock()
219
230
  self._handle: CommandHandle | None = None
220
231
  self._cancel_requested = False
@@ -230,9 +241,11 @@ class AgentSession:
230
241
  """
231
242
  return self._agent.profile
232
243
 
233
- def adopt(self, session_id: str) -> bool:
244
+ def adopt(self, session_id: str, *, previous_usage: ProviderUsage | None = None) -> bool:
234
245
  """Continue an existing provider conversation on the next turn.
235
246
 
247
+ ``previous_usage`` seeds an external conversation's last raw report.
248
+ Reports from conversations already seen by this session are retained.
236
249
  Returns ``False`` when the provider cannot resume, or when a turn is
237
250
  in flight and switching conversations would race it.
238
251
  """
@@ -242,6 +255,8 @@ class AgentSession:
242
255
  if not self._idle.is_set():
243
256
  return False
244
257
  self.session_id = session_id
258
+ if previous_usage is not None:
259
+ self._usage_by_session[session_id] = previous_usage
245
260
  return True
246
261
 
247
262
  def forget(self) -> bool:
@@ -348,7 +363,19 @@ class AgentSession:
348
363
  handler.on_event(event)
349
364
 
350
365
  argv = list(command.argv)
351
- parser = agent.provider.new_parser(emit, expect_structured=expect_structured)
366
+ parser = agent.provider.new_parser(
367
+ emit,
368
+ expect_structured=expect_structured,
369
+ )
370
+ if isinstance(parser, ContextualStreamParser):
371
+ parser.configure(
372
+ ParserContext(
373
+ previous_usage=(
374
+ self._usage_by_session.get(self.session_id) if self.session_id else None
375
+ ),
376
+ resumed=resumed,
377
+ )
378
+ )
352
379
  emit(RunStarted(tuple(argv)))
353
380
  started = time.monotonic()
354
381
  try:
@@ -359,6 +386,7 @@ class AgentSession:
359
386
  duration_ms = int((time.monotonic() - started) * 1000)
360
387
  emit(RunFinished(result.returncode))
361
388
  parsed = parser.finish()
389
+ self._remember_usage(parsed)
362
390
  self._adopt_then_report(parsed, result, argv, resumed=resumed)
363
391
 
364
392
  turn_result = TurnResult(
@@ -393,11 +421,18 @@ class AgentSession:
393
421
  """
394
422
  emit(RunFinished(None))
395
423
  partial = parser.finish()
424
+ self._remember_usage(partial)
396
425
  if partial.session_id:
397
426
  self.session_id = partial.session_id
398
427
  if isinstance(error, CliTimeoutError):
399
428
  error.partial = partial
400
429
 
430
+ def _remember_usage(self, parsed: ParsedTurn) -> None:
431
+ """Retain raw totals even on a failed invocation, without erasing a baseline."""
432
+ session_id = parsed.session_id or self.session_id
433
+ if session_id is not None and parsed.usage.raw is not None:
434
+ self._usage_by_session[session_id] = parsed.usage
435
+
401
436
  def _adopt_then_report(
402
437
  self,
403
438
  parsed: ParsedTurn,
@@ -65,7 +65,15 @@ from .profile import (
65
65
  SkillScope,
66
66
  SkillSignal,
67
67
  )
68
- from .provider import ArgvContext, McpInstallation, ParsedTurn, Provider, StreamParser
68
+ from .provider import (
69
+ ArgvContext,
70
+ ContextualStreamParser,
71
+ McpInstallation,
72
+ ParsedTurn,
73
+ ParserContext,
74
+ Provider,
75
+ StreamParser,
76
+ )
69
77
  from .schema import compact_json, dialect_problems, materialize, normalize
70
78
  from .skills import SkillSummary, SkillTracker
71
79
  from .stream import ToolTracker, parse_json_object
@@ -86,6 +94,7 @@ __all__ = [
86
94
  "ConfigFileInstallation",
87
95
  "ConfigScope",
88
96
  "ConsoleEventHandler",
97
+ "ContextualStreamParser",
89
98
  "EventHandlerBase",
90
99
  "FailureKind",
91
100
  "FlagsInstallation",
@@ -103,6 +112,7 @@ __all__ = [
103
112
  "OutputSchema",
104
113
  "OutputSchemaStyle",
105
114
  "ParsedTurn",
115
+ "ParserContext",
106
116
  "PricingTable",
107
117
  "Provider",
108
118
  "ProviderCapabilityError",
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, field
6
- from typing import TYPE_CHECKING, Any, Protocol
6
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
7
7
 
8
8
  from .errors import FailureKind
9
9
  from .profile import ConfigScope, McpScope, SkillScope
@@ -99,6 +99,23 @@ class StreamParser(Protocol):
99
99
  ...
100
100
 
101
101
 
102
+ @dataclass(frozen=True)
103
+ class ParserContext:
104
+ """Optional invocation context for parsers that need cumulative usage."""
105
+
106
+ previous_usage: ProviderUsage | None = None
107
+ resumed: bool = False
108
+
109
+
110
+ @runtime_checkable
111
+ class ContextualStreamParser(Protocol):
112
+ """Optional parser extension, leaving existing provider factories compatible."""
113
+
114
+ def configure(self, context: ParserContext) -> None:
115
+ """Set invocation context once, before any stream lines are fed."""
116
+ ...
117
+
118
+
102
119
  class McpInstallation(Protocol):
103
120
  """The undo record for MCP servers installed for one turn."""
104
121
 
@@ -140,7 +157,9 @@ class Provider(Protocol):
140
157
 
141
158
  A parser is single-use. ``expect_structured`` says the caller asked for
142
159
  an output schema, which is what lets a parser tell a missing structured
143
- result apart from a turn that never wanted one.
160
+ result apart from a turn that never wanted one. Parsers that implement
161
+ ``ContextualStreamParser`` receive a ``ParserContext`` before streaming.
162
+ Existing parsers need not implement that optional extension.
144
163
  """
145
164
  ...
146
165
 
@@ -267,12 +267,17 @@ class ProviderUsage:
267
267
 
268
268
  ``raw`` keeps the last usage mapping the CLI printed so a caller can
269
269
  diagnose a normalization gap without re-parsing the stream.
270
+ ``increment_known=False`` means the invocation's token increment could
271
+ not be reconstructed (for example, a Codex resume without a baseline).
272
+ Its token counts are zero placeholders, with ``turns`` still counted;
273
+ callers must check this marker before pricing or budgeting the counts.
270
274
  """
271
275
 
272
276
  tokens: TokenUsage = field(default_factory=TokenUsage)
273
277
  total_cost_usd: float | None = None
274
278
  provider: str = ""
275
279
  raw: Mapping[str, Any] | None = None
280
+ increment_known: bool = True
276
281
 
277
282
  def to_dict(self) -> dict[str, Any]:
278
283
  """Flatten the normalized counts, cost, and provider name into one mapping.
@@ -284,4 +289,5 @@ class ProviderUsage:
284
289
  **self.tokens.to_dict(),
285
290
  "total_cost_usd": self.total_cost_usd,
286
291
  "provider": self.provider,
292
+ "increment_known": self.increment_known,
287
293
  }
@@ -34,7 +34,7 @@ class TurnStarted:
34
34
 
35
35
  @dataclass(frozen=True)
36
36
  class TurnCompleted:
37
- """``{"type":"turn.completed","usage":{...}}``: one turn's token counts.
37
+ """``{"type":"turn.completed","usage":{...}}``: cumulative thread token counts.
38
38
 
39
39
  Codex's ``input_tokens`` already includes ``cached_input_tokens`` (cache
40
40
  reads) and ``cache_write_input_tokens``, and ``output_tokens`` already
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ from dataclasses import replace
6
7
  from typing import TYPE_CHECKING
7
8
 
8
9
  from agentshim.core.events import (
@@ -17,7 +18,7 @@ from agentshim.core.events import (
17
18
  ToolResult,
18
19
  UsageReport,
19
20
  )
20
- from agentshim.core.provider import ParsedTurn
21
+ from agentshim.core.provider import ParsedTurn, ParserContext
21
22
  from agentshim.core.stream import ToolTracker, parse_json_object
22
23
  from agentshim.core.usage import ProviderUsage, TokenUsage, normalized_usage
23
24
 
@@ -53,20 +54,20 @@ COMMAND_TOOL = "execute"
53
54
  FAILED_ITEM_STATUS = "failed"
54
55
 
55
56
 
56
- def fold_usage(frame: TurnCompleted, previous: TokenUsage) -> TokenUsage:
57
- """Add one ``turn.completed`` frame's counts to the running total.
57
+ def fold_usage(frame: TurnCompleted, previous: TurnCompleted) -> TokenUsage:
58
+ """Subtract the previous thread total from Codex's cumulative counts.
58
59
 
59
- Codex's counts are already nested the way agentshim normalizes them:
60
- ``input_tokens`` includes cache reads (``cached_input_tokens``) and cache
61
- writes, and ``output_tokens`` includes ``reasoning_output_tokens``. So
62
- unlike Claude nothing is folded in; adding them would double-count.
60
+ Exec JSON exports the thread's total, including on resume, with no
61
+ per-invocation field. Input already includes cache reads and writes;
62
+ output already includes reasoning. Subtract before normalizing so an
63
+ inconsistent provider breakdown cannot distort the saved baseline.
63
64
  """
64
- return previous + normalized_usage(
65
- input_tokens=frame.input_tokens,
66
- output_tokens=frame.output_tokens,
67
- cache_read_input_tokens=frame.cached_input_tokens,
68
- cache_write_input_tokens=frame.cache_write_input_tokens,
69
- reasoning_output_tokens=frame.reasoning_output_tokens,
65
+ return normalized_usage(
66
+ input_tokens=frame.input_tokens - previous.input_tokens,
67
+ output_tokens=frame.output_tokens - previous.output_tokens,
68
+ cache_read_input_tokens=frame.cached_input_tokens - previous.cached_input_tokens,
69
+ cache_write_input_tokens=frame.cache_write_input_tokens - previous.cache_write_input_tokens,
70
+ reasoning_output_tokens=frame.reasoning_output_tokens - previous.reasoning_output_tokens,
70
71
  turns=1,
71
72
  )
72
73
 
@@ -79,6 +80,8 @@ class CodexStreamParser:
79
80
  emit: Callable[[AgentEvent], None],
80
81
  *,
81
82
  expect_structured: bool = False,
83
+ previous_usage: ProviderUsage | None = None,
84
+ resumed: bool = False,
82
85
  ) -> None:
83
86
  """Build a parser that emits through *emit*.
84
87
 
@@ -86,6 +89,10 @@ class CodexStreamParser:
86
89
  emit: Sink for every event this run produces.
87
90
  expect_structured: Whether the turn asked Codex for a schema, in
88
91
  which case the final message is decoded as JSON.
92
+ previous_usage: The prior invocation's report. Its raw counts,
93
+ rather than its normalized increment, are the thread baseline.
94
+ resumed: Whether this invocation resumes an existing thread. A
95
+ completed resume without a baseline cannot be accounted for.
89
96
  """
90
97
  self._emit = emit
91
98
  self._expect_structured = expect_structured
@@ -94,11 +101,30 @@ class CodexStreamParser:
94
101
  # Codex has no terminal result frame: the last agent_message is the
95
102
  # answer, and with a schema it is the structured payload verbatim.
96
103
  self._final_text: str | None = None
104
+ self.configure(ParserContext(previous_usage=previous_usage, resumed=resumed))
105
+ self._completed_turns = 0
97
106
  self._tokens = TokenUsage()
98
- self._usage = ProviderUsage(provider=PROVIDER_NAME)
99
107
  self._error: str | None = None
100
108
  self._stderr: list[str] = []
101
109
 
110
+ def configure(self, context: ParserContext) -> None:
111
+ """Set the fixed starting baseline before streaming this invocation."""
112
+ baseline = None
113
+ if (
114
+ context.previous_usage is not None
115
+ and context.previous_usage.provider == PROVIDER_NAME
116
+ and context.previous_usage.raw is not None
117
+ ):
118
+ baseline = parse_frame(
119
+ {"type": "turn.completed", "usage": dict(context.previous_usage.raw)}
120
+ )
121
+ self._baseline = baseline if isinstance(baseline, TurnCompleted) else TurnCompleted()
122
+ self._baseline_known = not context.resumed or (
123
+ isinstance(baseline, TurnCompleted) and baseline.usage is not None
124
+ )
125
+
126
+ self._usage = ProviderUsage(provider=PROVIDER_NAME, increment_known=self._baseline_known)
127
+
102
128
  def feed_stdout(self, line: str) -> None:
103
129
  """Parse one stdout line, emitting whatever events it carries."""
104
130
  data = parse_json_object(line)
@@ -159,12 +185,18 @@ class CodexStreamParser:
159
185
  self._emit(Lifecycle("thread_started", frame.thread_id))
160
186
 
161
187
  def _turn_completed(self, frame: TurnCompleted) -> None:
162
- self._tokens = fold_usage(frame, self._tokens)
188
+ self._completed_turns += 1
189
+ if frame.usage is not None and self._baseline_known:
190
+ delta = fold_usage(frame, self._baseline)
191
+ self._tokens = replace(delta, turns=self._completed_turns)
192
+ else:
193
+ self._tokens = self._tokens + TokenUsage(turns=1)
163
194
  self._usage = ProviderUsage(
164
195
  tokens=self._tokens,
165
196
  total_cost_usd=None,
166
197
  provider=PROVIDER_NAME,
167
198
  raw=frame.usage if frame.usage is not None else self._usage.raw,
199
+ increment_known=self._baseline_known,
168
200
  )
169
201
  if frame.usage is None:
170
202
  self._emit(Lifecycle("turn_completed", ""))
@@ -188,7 +188,10 @@ class CodexProvider:
188
188
  expect_structured: bool,
189
189
  ) -> CodexStreamParser:
190
190
  """Build a stream parser for one run."""
191
- return CodexStreamParser(emit, expect_structured=expect_structured)
191
+ return CodexStreamParser(
192
+ emit,
193
+ expect_structured=expect_structured,
194
+ )
192
195
 
193
196
  def install_mcp(self, workspace: Path | None, servers: Sequence[McpServer]) -> McpInstallation:
194
197
  """Render *servers* as ``--config mcp_servers.*`` flags.
@@ -40,7 +40,8 @@ def scripted_lines( # noqa: PLR0913
40
40
  Args:
41
41
  text: The assistant's final message.
42
42
  session_id: Thread id announced by ``thread.started``.
43
- usage: Token counts reported by ``turn.completed``.
43
+ usage: Cumulative thread counts reported by ``turn.completed``.
44
+ For a resumed turn, include the earlier invocations too.
44
45
  tool_calls: Commands to report as started-then-completed items.
45
46
  structured_output: Payload to serialize as the final message.
46
47
  skills_offered: Must be None; the stream lists no offered skills.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentshim"
3
- version = "0.13.1"
3
+ version = "0.14.0"
4
4
  description = "Provider-agnostic coding-agent CLI shims"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -0,0 +1,18 @@
1
+ # Codex usage fixtures
2
+
3
+ The existing `exec_gpt6_luna.jsonl` and skill fixtures are recorded CLI stdout.
4
+ Their usage mappings are unchanged.
5
+
6
+ `resume_vibesys_totals.jsonl` is a minimal reconstruction of the two input
7
+ counts supplied in the VibeSys bug report: 2,019,100 followed by 12,564,014
8
+ for the same thread. The thread id is synthetic. Output, cache and reasoning
9
+ counts were not supplied, so these fields are omitted rather than invented.
10
+ This is not a newly recorded CLI run. Codex 0.160.0 was found on the
11
+ interactive shell's PATH, but the requested `gpt-6.1-sol` / medium probe
12
+ failed to initialize its app-server because the unchanged Codex home is
13
+ read-only in this sandbox. It emitted no JSON events. The repository
14
+ contained no two-invocation recording.
15
+
16
+ Codex 0.144.4's JSON event processor copies all five fields from the thread
17
+ usage `total` into `turn.completed.usage`; it does not expose a per-turn field:
18
+ [provider source](https://github.com/openai/codex/blob/rust-v0.144.4/codex-rs/exec/src/event_processor_with_jsonl_output.rs).
File without changes
File without changes