agentshim 0.13.1__tar.gz → 0.14.1__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.1}/CHANGELOG.md +33 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/PKG-INFO +1 -1
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/__init__.py +5 -1
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/agent.py +40 -5
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/__init__.py +11 -1
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/provider.py +21 -2
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/schema.py +70 -26
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/usage.py +6 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/events.py +1 -1
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/parser.py +47 -15
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/provider.py +4 -1
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/scripted.py +2 -1
- {agentshim-0.13.1 → agentshim-0.14.1}/pyproject.toml +1 -1
- agentshim-0.14.1/tests/fixtures/codex/README.md +18 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/.gitignore +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/README.md +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/_files.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/env.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/errors.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/home.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/mcp.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/pricing.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/profile.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/skills.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/stream.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/core/turn.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/execution/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/execution/executor.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/execution/host.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/execution/transform.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/failures.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/hooks/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/parser.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/provider.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/sandbox.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/scripted.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/claude/user_hooks.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/_toml.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/failures.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/mcp_scope.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/rules.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/sandbox.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/codex/skills.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/copilot/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/copilot/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/copilot/parser.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/copilot/provider.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/copilot/scripted.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/gemini/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/gemini/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/gemini/parser.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/gemini/provider.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/gemini/scripted.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/opencode/__init__.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/opencode/events.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/opencode/parser.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/opencode/provider.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/providers/opencode/scripted.py +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/py.typed +0 -0
- {agentshim-0.13.1 → agentshim-0.14.1}/agentshim/testing/__init__.py +0 -0
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.1 (2026-10-04)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Schema normalization preserves descriptions, titles and examples, including
|
|
8
|
+
annotations alongside `$ref`, so structured-output field guidance reaches
|
|
9
|
+
the model. Only metadata rejected by the selected dialect is removed.
|
|
10
|
+
- The open schema dialect accepts `oneOf` variants, as Claude does.
|
|
11
|
+
|
|
12
|
+
## 0.14.0 (2026-10-04)
|
|
13
|
+
|
|
14
|
+
Codex resumed turns report invocation usage. Additive.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `ProviderUsage.increment_known` marks whether the invocation's token
|
|
19
|
+
increment is measurable. Unknown increments retain raw cumulative usage
|
|
20
|
+
and zero token placeholders; consumers must check the marker before
|
|
21
|
+
pricing or budgeting.
|
|
22
|
+
- `previous_usage` on session creation and adoption seeds a checkpointed
|
|
23
|
+
conversation's usage baseline. `ParserContext` and `ContextualStreamParser`
|
|
24
|
+
let providers receive invocation context without changing provider factories.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Codex `turn.completed` usage is cumulative per thread. Resumed turns now
|
|
29
|
+
subtract the prior raw usage report for every token class instead of
|
|
30
|
+
counting earlier tokens again. Repeated completion snapshots replace the
|
|
31
|
+
previous snapshot, and conversations retain independent baselines,
|
|
32
|
+
including after failed invocations.
|
|
33
|
+
- Package `__version__` and its test now match the project metadata; the
|
|
34
|
+
0.13.1 release left both at 0.13.0.
|
|
35
|
+
|
|
3
36
|
## 0.13.1 (2026-10-03)
|
|
4
37
|
|
|
5
38
|
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.1"
|
|
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
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Providers disagree about which JSON Schema subset their structured-output
|
|
4
4
|
flag accepts, and the failure mode is an expensive agent turn that ends in a
|
|
5
|
-
CLI parse error. These checks run before the process starts.
|
|
5
|
+
CLI parse error. These checks run before the process starts. Real CLI probes
|
|
6
|
+
on 2026-10-04 confirmed that Codex and Claude accept descriptions, titles
|
|
7
|
+
and examples; normalization preserves these annotations as model guidance.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
@@ -30,8 +32,6 @@ UNSUPPORTED_KEYWORDS = frozenset(
|
|
|
30
32
|
"$anchor",
|
|
31
33
|
"$dynamicAnchor",
|
|
32
34
|
"$dynamicRef",
|
|
33
|
-
"$id",
|
|
34
|
-
"$schema",
|
|
35
35
|
"allOf",
|
|
36
36
|
"contains",
|
|
37
37
|
"dependentRequired",
|
|
@@ -41,7 +41,6 @@ UNSUPPORTED_KEYWORDS = frozenset(
|
|
|
41
41
|
"maxContains",
|
|
42
42
|
"minContains",
|
|
43
43
|
"not",
|
|
44
|
-
"oneOf",
|
|
45
44
|
"patternProperties",
|
|
46
45
|
"prefixItems",
|
|
47
46
|
"propertyNames",
|
|
@@ -53,17 +52,28 @@ UNSUPPORTED_KEYWORDS = frozenset(
|
|
|
53
52
|
|
|
54
53
|
_SUBSCHEMA_MAPS = frozenset({"properties", "$defs", "definitions"})
|
|
55
54
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# them. Field names are never matched against this set: ``properties`` is
|
|
60
|
-
# traversed as a map of subschemas, so a property named ``title`` survives.
|
|
55
|
+
# Metadata values are annotations or document identifiers, never subschemas.
|
|
56
|
+
# Preserve model guidance, including annotations alongside a $ref. Field
|
|
57
|
+
# names are never matched against this set: properties is a subschema map.
|
|
61
58
|
_METADATA_KEYWORDS = frozenset({"$schema", "$id", "title", "description", "examples"})
|
|
62
59
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
60
|
+
# Dialect policy is shared by checking and normalization. Codex's strict
|
|
61
|
+
# subset rejects document identifiers; open schemas tolerate them. Neither
|
|
62
|
+
# dialect rejects description/title/examples in the measured CLI versions.
|
|
63
|
+
_REJECTED_METADATA: dict[SchemaDialect, frozenset[str]] = {
|
|
64
|
+
SchemaDialect.STRICT: frozenset({"$schema", "$id"}),
|
|
65
|
+
SchemaDialect.OPEN: frozenset(),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Claude accepts oneOf variants, while Codex's strict subset rejects oneOf.
|
|
69
|
+
_DIALECT_UNSUPPORTED = {
|
|
70
|
+
SchemaDialect.STRICT: UNSUPPORTED_KEYWORDS | {"oneOf"},
|
|
71
|
+
SchemaDialect.OPEN: UNSUPPORTED_KEYWORDS,
|
|
72
|
+
}
|
|
73
|
+
_REF_ANNOTATIONS_ALLOWED = {
|
|
74
|
+
SchemaDialect.STRICT: False,
|
|
75
|
+
SchemaDialect.OPEN: True,
|
|
76
|
+
}
|
|
67
77
|
|
|
68
78
|
# Keywords whose values are data, never subschemas. ``required`` is a list of
|
|
69
79
|
# field names and ``enum``/``const`` hold instance values, so a name such as
|
|
@@ -84,7 +94,10 @@ def dialect_problems(schema: Mapping[str, Any], dialect: SchemaDialect) -> list[
|
|
|
84
94
|
``additionalProperties: false`` and lists exactly its ``properties`` in
|
|
85
95
|
``required`` (an optional value is
|
|
86
96
|
expressed as nullable instead), the root is not an ``anyOf``, and every
|
|
87
|
-
``$ref`` resolves inside the document.
|
|
97
|
+
``$ref`` resolves inside the document. Real CLI probes on 2026-10-04
|
|
98
|
+
confirmed both dialects accept ``description``, ``title`` and ``examples``
|
|
99
|
+
on schema nodes. ``STRICT`` rejects them beside ``$ref``; ``OPEN`` accepts
|
|
100
|
+
those siblings and ``oneOf`` variants.
|
|
88
101
|
"""
|
|
89
102
|
root = dict(schema)
|
|
90
103
|
walk = _Walk(root=root, dialect=dialect, problems=[])
|
|
@@ -125,9 +138,9 @@ class _Walk:
|
|
|
125
138
|
def visit(self, node: object, location: str) -> None:
|
|
126
139
|
"""Walk one schema node, appending a problem for every unsupported construct.
|
|
127
140
|
|
|
128
|
-
A ``$ref`` node terminates the walk
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
A ``$ref`` node terminates the walk after checking its metadata. The
|
|
142
|
+
target is checked where it is defined, since every local target is
|
|
143
|
+
inside the document this walk covers.
|
|
131
144
|
"""
|
|
132
145
|
if isinstance(node, list):
|
|
133
146
|
for index, value in enumerate(cast("list[object]", node)):
|
|
@@ -140,6 +153,7 @@ class _Walk:
|
|
|
140
153
|
reference = mapping.get("$ref")
|
|
141
154
|
if reference is not None:
|
|
142
155
|
self._check_ref(reference, location)
|
|
156
|
+
self._check_ref_metadata(mapping, location)
|
|
143
157
|
return
|
|
144
158
|
|
|
145
159
|
if not self._check_properties_shape(mapping, location):
|
|
@@ -157,6 +171,17 @@ class _Walk:
|
|
|
157
171
|
if self.dialect is SchemaDialect.STRICT and not _resolves(self.root, reference):
|
|
158
172
|
self.problems.append(f"{location} has a $ref {reference!r} that resolves to nothing")
|
|
159
173
|
|
|
174
|
+
def _check_ref_metadata(self, mapping: dict[str, Any], location: str) -> None:
|
|
175
|
+
"""Check metadata siblings without traversing the referenced target twice."""
|
|
176
|
+
for key in mapping:
|
|
177
|
+
if key not in _METADATA_KEYWORDS:
|
|
178
|
+
continue
|
|
179
|
+
if (
|
|
180
|
+
key in _REJECTED_METADATA[self.dialect]
|
|
181
|
+
or not _REF_ANNOTATIONS_ALLOWED[self.dialect]
|
|
182
|
+
):
|
|
183
|
+
self.problems.append(f"{location} uses unsupported keyword {key!r} beside '$ref'")
|
|
184
|
+
|
|
160
185
|
def _check_properties_shape(self, mapping: dict[str, Any], location: str) -> bool:
|
|
161
186
|
"""Check that ``properties``, if present, is an object.
|
|
162
187
|
|
|
@@ -220,9 +245,9 @@ class _Walk:
|
|
|
220
245
|
for key, value in mapping.items():
|
|
221
246
|
if key in _SUBSCHEMA_MAPS:
|
|
222
247
|
self._visit_subschema_map(value, f"{location}/{key}")
|
|
223
|
-
elif
|
|
224
|
-
|
|
225
|
-
|
|
248
|
+
elif (
|
|
249
|
+
key in _DIALECT_UNSUPPORTED[self.dialect] or key in _REJECTED_METADATA[self.dialect]
|
|
250
|
+
):
|
|
226
251
|
self.problems.append(f"{location} uses unsupported keyword {key!r}")
|
|
227
252
|
elif key in _METADATA_KEYWORDS or key in _NON_SCHEMA_KEYWORDS:
|
|
228
253
|
# Annotations or plain values: nothing below them is a schema.
|
|
@@ -269,10 +294,13 @@ def normalize(schema: Mapping[str, Any], dialect: SchemaDialect) -> dict[str, An
|
|
|
269
294
|
Generators such as Pydantic omit defaulted properties from ``required``
|
|
270
295
|
and leave ``additionalProperties`` unset, which the CLIs read as "any
|
|
271
296
|
subset of these keys, plus anything else". This closes every object,
|
|
272
|
-
requires every declared property
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
``
|
|
297
|
+
requires every declared property and drops ``default``. Descriptions,
|
|
298
|
+
titles and examples are retained at every schema node: Codex and Claude
|
|
299
|
+
accepted them in real CLI probes on 2026-10-04. For ``STRICT``, annotations
|
|
300
|
+
beside ``$ref`` move to an ``anyOf`` wrapper containing the reference;
|
|
301
|
+
``OPEN`` keeps them as siblings.
|
|
302
|
+
Only metadata rejected by the dialect is removed (``$schema`` and ``$id``
|
|
303
|
+
under ``STRICT``). Other ``$ref`` siblings are stripped as before. An open
|
|
276
304
|
map (``additionalProperties`` a schema or ``true``) is kept, since closing
|
|
277
305
|
it would change what the schema accepts; so ``dialect_problems`` on the
|
|
278
306
|
normalized schema reports exactly what the dialect cannot express.
|
|
@@ -290,10 +318,10 @@ def _normalized(node: object, dialect: SchemaDialect) -> object:
|
|
|
290
318
|
|
|
291
319
|
reference = mapping.get("$ref")
|
|
292
320
|
if isinstance(reference, str):
|
|
293
|
-
return
|
|
321
|
+
return _normalized_ref(mapping, dialect, reference)
|
|
294
322
|
|
|
295
323
|
mapping.pop("default", None)
|
|
296
|
-
for annotation in
|
|
324
|
+
for annotation in _REJECTED_METADATA[dialect]:
|
|
297
325
|
mapping.pop(annotation, None)
|
|
298
326
|
properties = mapping.get("properties")
|
|
299
327
|
if isinstance(properties, dict):
|
|
@@ -305,6 +333,8 @@ def _normalized(node: object, dialect: SchemaDialect) -> object:
|
|
|
305
333
|
mapping["required"] = []
|
|
306
334
|
|
|
307
335
|
for key, value in list(mapping.items()):
|
|
336
|
+
if key in _METADATA_KEYWORDS or key in _NON_SCHEMA_KEYWORDS:
|
|
337
|
+
continue
|
|
308
338
|
if key in _SUBSCHEMA_MAPS and isinstance(value, dict):
|
|
309
339
|
mapping[key] = {
|
|
310
340
|
name: _normalized(subschema, dialect)
|
|
@@ -315,6 +345,20 @@ def _normalized(node: object, dialect: SchemaDialect) -> object:
|
|
|
315
345
|
return mapping
|
|
316
346
|
|
|
317
347
|
|
|
348
|
+
def _normalized_ref(
|
|
349
|
+
mapping: dict[str, Any], dialect: SchemaDialect, reference: str
|
|
350
|
+
) -> dict[str, Any]:
|
|
351
|
+
"""Keep accepted annotations without introducing forbidden $ref siblings."""
|
|
352
|
+
annotations = {
|
|
353
|
+
key: value
|
|
354
|
+
for key, value in mapping.items()
|
|
355
|
+
if key in _METADATA_KEYWORDS - _REJECTED_METADATA[dialect]
|
|
356
|
+
}
|
|
357
|
+
if annotations and not _REF_ANNOTATIONS_ALLOWED[dialect]:
|
|
358
|
+
return {"anyOf": [{"$ref": reference}], **annotations}
|
|
359
|
+
return {"$ref": reference, **annotations}
|
|
360
|
+
|
|
361
|
+
|
|
318
362
|
def _close_object(mapping: dict[str, Any], dialect: SchemaDialect) -> None:
|
|
319
363
|
"""Close an object whose extra keys are merely unspecified.
|
|
320
364
|
|
|
@@ -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
|