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.
- {agentshim-0.13.1 → agentshim-0.14.0}/CHANGELOG.md +24 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/PKG-INFO +1 -1
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/__init__.py +5 -1
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/agent.py +40 -5
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/__init__.py +11 -1
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/provider.py +21 -2
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/usage.py +6 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/events.py +1 -1
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/parser.py +47 -15
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/provider.py +4 -1
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/scripted.py +2 -1
- {agentshim-0.13.1 → agentshim-0.14.0}/pyproject.toml +1 -1
- agentshim-0.14.0/tests/fixtures/codex/README.md +18 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/.gitignore +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/README.md +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/_files.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/env.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/errors.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/home.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/mcp.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/pricing.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/profile.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/schema.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/skills.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/stream.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/core/turn.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/execution/executor.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/execution/host.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/execution/transform.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/failures.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/parser.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/provider.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/failures.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/mcp_scope.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.0}/agentshim/py.typed +0 -0
- {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.
|
|
@@ -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.
|
|
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. ``
|
|
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(
|
|
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
|
|
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":{...}}``:
|
|
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:
|
|
57
|
-
"""
|
|
57
|
+
def fold_usage(frame: TurnCompleted, previous: TurnCompleted) -> TokenUsage:
|
|
58
|
+
"""Subtract the previous thread total from Codex's cumulative counts.
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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.
|
|
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(
|
|
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:
|
|
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.
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|