agentlings 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {agentlings-0.2.2 → agentlings-0.2.3}/PKG-INFO +1 -1
  2. {agentlings-0.2.2 → agentlings-0.2.3}/pyproject.toml +1 -1
  3. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/a2a.py +16 -2
  4. agentlings-0.2.3/tests/unit/test_a2a_executor.py +292 -0
  5. {agentlings-0.2.2 → agentlings-0.2.3}/.env.example +0 -0
  6. {agentlings-0.2.2 → agentlings-0.2.3}/.github/workflows/ci.yml +0 -0
  7. {agentlings-0.2.2 → agentlings-0.2.3}/.github/workflows/publish.yml +0 -0
  8. {agentlings-0.2.2 → agentlings-0.2.3}/.gitignore +0 -0
  9. {agentlings-0.2.2 → agentlings-0.2.3}/CLAUDE.md +0 -0
  10. {agentlings-0.2.2 → agentlings-0.2.3}/DESIGN-memory-sleep.md +0 -0
  11. {agentlings-0.2.2 → agentlings-0.2.3}/Dockerfile +0 -0
  12. {agentlings-0.2.2 → agentlings-0.2.3}/LICENSE +0 -0
  13. {agentlings-0.2.2 → agentlings-0.2.3}/README.md +0 -0
  14. {agentlings-0.2.2 → agentlings-0.2.3}/agent.example.yaml +0 -0
  15. {agentlings-0.2.2 → agentlings-0.2.3}/docker-compose.test.yml +0 -0
  16. {agentlings-0.2.2 → agentlings-0.2.3}/logo.png +0 -0
  17. {agentlings-0.2.2 → agentlings-0.2.3}/scripts/release.sh +0 -0
  18. {agentlings-0.2.2 → agentlings-0.2.3}/sleep.png +0 -0
  19. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/__init__.py +0 -0
  20. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/__main__.py +0 -0
  21. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/__init__.py +0 -0
  22. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/_migrations.py +0 -0
  23. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/_templates.py +0 -0
  24. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/_version.py +0 -0
  25. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/init.py +0 -0
  26. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/cli/upgrade.py +0 -0
  27. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/config.py +0 -0
  28. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/__init__.py +0 -0
  29. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/completion.py +0 -0
  30. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/llm.py +0 -0
  31. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/loop.py +0 -0
  32. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/memory_models.py +0 -0
  33. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/memory_store.py +0 -0
  34. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/models.py +0 -0
  35. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/prompt.py +0 -0
  36. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/scheduler.py +0 -0
  37. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/sleep.py +0 -0
  38. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/store.py +0 -0
  39. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/task.py +0 -0
  40. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/core/telemetry.py +0 -0
  41. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/log.py +0 -0
  42. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/migrations/__init__.py +0 -0
  43. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/migrations/m0001_seed.py +0 -0
  44. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/__init__.py +0 -0
  45. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/a2a_task_store.py +0 -0
  46. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/agent_card.py +0 -0
  47. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/protocol/mcp.py +0 -0
  48. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/server.py +0 -0
  49. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/templates/__init__.py +0 -0
  50. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/templates/default/.env.example +0 -0
  51. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/templates/default/agent.yaml +0 -0
  52. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/tools/__init__.py +0 -0
  53. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/tools/builtins.py +0 -0
  54. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/tools/memory.py +0 -0
  55. {agentlings-0.2.2 → agentlings-0.2.3}/src/agentlings/tools/registry.py +0 -0
  56. {agentlings-0.2.2 → agentlings-0.2.3}/tests/Dockerfile +0 -0
  57. {agentlings-0.2.2 → agentlings-0.2.3}/tests/__init__.py +0 -0
  58. {agentlings-0.2.2 → agentlings-0.2.3}/tests/agent.test.yaml +0 -0
  59. {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/__init__.py +0 -0
  60. {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/a2a_client.py +0 -0
  61. {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/conftest.py +0 -0
  62. {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/mcp_client.py +0 -0
  63. {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_a2a.py +0 -0
  64. {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_agent_card.py +0 -0
  65. {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_mcp.py +0 -0
  66. {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_ollama.py +0 -0
  67. {agentlings-0.2.2 → agentlings-0.2.3}/tests/integration/test_task_flow.py +0 -0
  68. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/__init__.py +0 -0
  69. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/conftest.py +0 -0
  70. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_a2a_task_store.py +0 -0
  71. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_agent_card.py +0 -0
  72. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_cli_init.py +0 -0
  73. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_cli_upgrade.py +0 -0
  74. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_completion.py +0 -0
  75. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_config.py +0 -0
  76. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_live_api.py +0 -0
  77. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_llm.py +0 -0
  78. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_logging.py +0 -0
  79. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_loop.py +0 -0
  80. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_mcp_handler.py +0 -0
  81. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_memory_models.py +0 -0
  82. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_memory_store.py +0 -0
  83. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_memory_tool.py +0 -0
  84. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_models.py +0 -0
  85. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_prompt.py +0 -0
  86. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_scheduler.py +0 -0
  87. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_sleep.py +0 -0
  88. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_store.py +0 -0
  89. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_task.py +0 -0
  90. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_telemetry.py +0 -0
  91. {agentlings-0.2.2 → agentlings-0.2.3}/tests/unit/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentlings
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Lightweight A2A + MCP single-process agent framework
5
5
  Project-URL: Homepage, https://github.com/andyjmorgan/DonkeyWork-Agentlings
6
6
  Project-URL: Repository, https://github.com/andyjmorgan/DonkeyWork-Agentlings
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentlings"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Lightweight A2A + MCP single-process agent framework"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -7,6 +7,11 @@ A2A ``Task`` object (``status.state == working``) is enqueued so the caller
7
7
  can poll via ``GetTask`` — those GetTask calls are routed back to our engine
8
8
  by ``EngineTaskStore`` so the SDK's answers always reflect live state.
9
9
 
10
+ Clients can opt out of the await window per-request by setting
11
+ ``configuration.return_immediately = true`` on ``message/send``; in that
12
+ case the executor passes ``await_seconds=0`` to the engine and a ``Task``
13
+ object is enqueued without blocking.
14
+
10
15
  Cancellation (``CancelTask``) hits the engine's cancel path by task id.
11
16
  """
12
17
 
@@ -78,10 +83,19 @@ class AgentlingExecutor(AgentExecutor):
78
83
  # through EngineTaskStore) and the Task object we enqueue all agree.
79
84
  sdk_task_id = context.task_id
80
85
 
86
+ # A2A 1.0: clients can opt out of blocking by setting
87
+ # ``configuration.return_immediately``. When set, skip the await
88
+ # window so a Task handle is enqueued immediately.
89
+ return_immediately = bool(
90
+ getattr(context.configuration, "return_immediately", False)
91
+ )
92
+ await_seconds = 0.0 if return_immediately else self._await_seconds
93
+
81
94
  logger.debug(
82
- "a2a execute: context_id=%s task_id=%s text=%r",
95
+ "a2a execute: context_id=%s task_id=%s return_immediately=%s text=%r",
83
96
  context_id,
84
97
  sdk_task_id,
98
+ return_immediately,
85
99
  (user_text or "")[:100],
86
100
  )
87
101
 
@@ -90,7 +104,7 @@ class AgentlingExecutor(AgentExecutor):
90
104
  message=user_text,
91
105
  context_id=context_id,
92
106
  via="a2a",
93
- await_seconds=self._await_seconds,
107
+ await_seconds=await_seconds,
94
108
  task_id=sdk_task_id,
95
109
  )
96
110
  except ContextBusyError as e:
@@ -0,0 +1,292 @@
1
+ """Tests for ``AgentlingExecutor`` covering the A2A SendMessage entrypoint.
2
+
3
+ Focus is on protocol-level wiring: that ``configuration.return_immediately``
4
+ is honored as a per-request opt-out of the await window, and that the
5
+ default behavior continues to block until ``AGENT_TASK_AWAIT_SECONDS``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+ from a2a.helpers.proto_helpers import new_text_message
15
+ from a2a.server.agent_execution.context import RequestContext
16
+ from a2a.server.context import ServerCallContext
17
+ from a2a.server.events import EventQueue
18
+ from a2a.types import (
19
+ Message,
20
+ Role,
21
+ SendMessageConfiguration,
22
+ SendMessageRequest,
23
+ Task,
24
+ )
25
+ from a2a.types import TaskState as A2ATaskState
26
+
27
+ from agentlings.config import AgentConfig
28
+ from agentlings.core.llm import LLMResponse
29
+ from agentlings.core.store import JournalStore
30
+ from agentlings.core.task import TaskEngine
31
+ from agentlings.protocol.a2a import AgentlingExecutor
32
+ from agentlings.tools.registry import ToolRegistry
33
+ from tests.unit.test_task import ControllableLLM
34
+
35
+
36
+ class _LoopShim:
37
+ """Minimal stand-in for ``MessageLoop`` exposing ``.engine`` only.
38
+
39
+ The executor's constructor reads ``loop.engine`` once at init and never
40
+ touches the loop again. Building the real ``MessageLoop`` would force
41
+ it to spin up its own internal ``TaskEngine``, defeating the point of
42
+ sharing the test-controlled engine.
43
+ """
44
+
45
+ def __init__(self, engine: TaskEngine) -> None:
46
+ self.engine = engine
47
+
48
+
49
+ def _make_request_context(
50
+ *,
51
+ text: str,
52
+ context_id: str,
53
+ task_id: str,
54
+ return_immediately: bool | None = None,
55
+ ) -> RequestContext:
56
+ msg = new_text_message(text, context_id=context_id, role=Role.ROLE_USER)
57
+ if return_immediately is None:
58
+ configuration = None
59
+ else:
60
+ configuration = SendMessageConfiguration(
61
+ return_immediately=return_immediately,
62
+ )
63
+ request = SendMessageRequest(message=msg, configuration=configuration)
64
+ return RequestContext(
65
+ call_context=ServerCallContext(),
66
+ request=request,
67
+ context_id=context_id,
68
+ task_id=task_id,
69
+ )
70
+
71
+
72
+ async def _execute_and_drain(
73
+ executor: AgentlingExecutor,
74
+ ctx: RequestContext,
75
+ queue: EventQueue,
76
+ *,
77
+ timeout: float,
78
+ ) -> tuple[list[object], float]:
79
+ """Run ``executor.execute`` and drain enqueued events concurrently.
80
+
81
+ Returns the list of events enqueued and the wall-clock seconds taken.
82
+ A concurrent consumer is required because ``EventQueue.close()`` blocks
83
+ on ``queue.join()`` until every enqueued event has been ``task_done``'d.
84
+ """
85
+
86
+ events: list[object] = []
87
+
88
+ async def consumer() -> None:
89
+ while True:
90
+ try:
91
+ event = await queue.dequeue_event()
92
+ except Exception: # queue closed
93
+ return
94
+ events.append(event)
95
+ queue.task_done()
96
+ if queue.is_closed() and queue.queue.empty():
97
+ return
98
+
99
+ start = asyncio.get_event_loop().time()
100
+ consumer_task = asyncio.create_task(consumer())
101
+ try:
102
+ await asyncio.wait_for(executor.execute(ctx, queue), timeout=timeout)
103
+ finally:
104
+ try:
105
+ await asyncio.wait_for(consumer_task, timeout=1.0)
106
+ except asyncio.TimeoutError:
107
+ consumer_task.cancel()
108
+ elapsed = asyncio.get_event_loop().time() - start
109
+ return events, elapsed
110
+
111
+
112
+ @pytest.fixture
113
+ def slow_engine(
114
+ tmp_data_dir: Path, test_config: AgentConfig
115
+ ) -> tuple[TaskEngine, ControllableLLM]:
116
+ """Engine wired to a ControllableLLM that never replies until told to.
117
+
118
+ This guarantees that any call to ``engine.spawn`` with a non-zero
119
+ ``await_seconds`` will burn the whole window — perfect for proving that
120
+ ``return_immediately`` collapses it to zero.
121
+ """
122
+ llm = ControllableLLM()
123
+ store = JournalStore(tmp_data_dir)
124
+ tools = ToolRegistry()
125
+ engine = TaskEngine(config=test_config, store=store, llm=llm, tools=tools)
126
+ return engine, llm
127
+
128
+
129
+ class TestReturnImmediately:
130
+ @pytest.mark.asyncio
131
+ async def test_return_immediately_yields_task_without_blocking(
132
+ self,
133
+ slow_engine: tuple[TaskEngine, ControllableLLM],
134
+ test_config: AgentConfig,
135
+ ) -> None:
136
+ engine, llm = slow_engine
137
+ # Configure a long await window — if the executor honored it, the
138
+ # test would hang for ~30s. With return_immediately=True it should
139
+ # return ~immediately.
140
+ test_config.agent_task_await_seconds = 30
141
+ loop = _LoopShim(engine)
142
+ executor = AgentlingExecutor(loop, test_config)
143
+
144
+ ctx = _make_request_context(
145
+ text="hello",
146
+ context_id="ctx-immediate",
147
+ task_id="task-immediate",
148
+ return_immediately=True,
149
+ )
150
+ queue = EventQueue()
151
+
152
+ # If the executor blocked on await_seconds (30), the 2s wall-clock
153
+ # cap would expire and fail the test.
154
+ events, elapsed = await _execute_and_drain(
155
+ executor, ctx, queue, timeout=2.0,
156
+ )
157
+ assert elapsed < 1.0, f"executor did not return immediately ({elapsed:.3f}s)"
158
+ assert len(events) == 1
159
+ event = events[0]
160
+ assert isinstance(event, Task), f"expected Task, got {type(event).__name__}"
161
+ assert event.id == "task-immediate"
162
+ assert event.context_id == "ctx-immediate"
163
+ assert event.status.state in (
164
+ A2ATaskState.TASK_STATE_WORKING,
165
+ A2ATaskState.TASK_STATE_SUBMITTED,
166
+ )
167
+
168
+ # Cleanup: let the still-running worker finish so the test doesn't
169
+ # leak a background task.
170
+ llm.push(LLMResponse(
171
+ content=[{"type": "text", "text": "done"}],
172
+ stop_reason="end_turn",
173
+ ))
174
+ rec = engine.registry.get("task-immediate")
175
+ if rec is not None:
176
+ await asyncio.wait_for(rec.completion_event.wait(), timeout=3.0)
177
+
178
+ @pytest.mark.asyncio
179
+ async def test_default_blocking_uses_configured_await(
180
+ self,
181
+ slow_engine: tuple[TaskEngine, ControllableLLM],
182
+ test_config: AgentConfig,
183
+ ) -> None:
184
+ """Without return_immediately, the executor blocks up to the
185
+ configured await window and surfaces a working Task when the worker
186
+ hasn't finished by then."""
187
+ engine, llm = slow_engine
188
+ test_config.agent_task_await_seconds = 0.2
189
+ loop = _LoopShim(engine)
190
+ executor = AgentlingExecutor(loop, test_config)
191
+
192
+ ctx = _make_request_context(
193
+ text="hello",
194
+ context_id="ctx-default",
195
+ task_id="task-default",
196
+ # configuration absent — represents a stock client that hasn't
197
+ # opted out of blocking.
198
+ return_immediately=None,
199
+ )
200
+ queue = EventQueue()
201
+
202
+ events, elapsed = await _execute_and_drain(
203
+ executor, ctx, queue, timeout=2.0,
204
+ )
205
+ # Must have waited approximately the configured window.
206
+ assert elapsed >= 0.15, f"executor returned too early ({elapsed:.3f}s)"
207
+ assert len(events) == 1
208
+ assert isinstance(events[0], Task)
209
+
210
+ # Cleanup.
211
+ llm.push(LLMResponse(
212
+ content=[{"type": "text", "text": "done"}],
213
+ stop_reason="end_turn",
214
+ ))
215
+ rec = engine.registry.get("task-default")
216
+ if rec is not None:
217
+ await asyncio.wait_for(rec.completion_event.wait(), timeout=3.0)
218
+
219
+ @pytest.mark.asyncio
220
+ async def test_return_immediately_false_still_blocks(
221
+ self,
222
+ slow_engine: tuple[TaskEngine, ControllableLLM],
223
+ test_config: AgentConfig,
224
+ ) -> None:
225
+ """An explicit ``return_immediately=False`` must behave like the
226
+ default — i.e. honor the configured await window."""
227
+ engine, llm = slow_engine
228
+ test_config.agent_task_await_seconds = 0.2
229
+ loop = _LoopShim(engine)
230
+ executor = AgentlingExecutor(loop, test_config)
231
+
232
+ ctx = _make_request_context(
233
+ text="hello",
234
+ context_id="ctx-explicit-blocking",
235
+ task_id="task-explicit-blocking",
236
+ return_immediately=False,
237
+ )
238
+ queue = EventQueue()
239
+
240
+ events, elapsed = await _execute_and_drain(
241
+ executor, ctx, queue, timeout=2.0,
242
+ )
243
+ assert elapsed >= 0.15, (
244
+ f"executor returned too early with return_immediately=False "
245
+ f"({elapsed:.3f}s)"
246
+ )
247
+ assert isinstance(events[0], Task)
248
+
249
+ llm.push(LLMResponse(
250
+ content=[{"type": "text", "text": "done"}],
251
+ stop_reason="end_turn",
252
+ ))
253
+ rec = engine.registry.get("task-explicit-blocking")
254
+ if rec is not None:
255
+ await asyncio.wait_for(rec.completion_event.wait(), timeout=3.0)
256
+
257
+ @pytest.mark.asyncio
258
+ async def test_fast_path_with_immediate_completion_returns_message(
259
+ self,
260
+ tmp_data_dir: Path,
261
+ test_config: AgentConfig,
262
+ ) -> None:
263
+ """If the LLM is fast (mock backend completes synchronously),
264
+ the default path still returns a Message, not a Task — proving
265
+ we haven't accidentally forced everything onto the slow path."""
266
+ from agentlings.core.llm import MockLLMClient
267
+
268
+ tools = ToolRegistry()
269
+ tools.register_tools(["bash", "filesystem"])
270
+ llm = MockLLMClient(tool_names=tools.tool_names())
271
+ store = JournalStore(tmp_data_dir)
272
+ engine = TaskEngine(
273
+ config=test_config, store=store, llm=llm, tools=tools,
274
+ )
275
+ loop = _LoopShim(engine)
276
+ executor = AgentlingExecutor(loop, test_config)
277
+
278
+ ctx = _make_request_context(
279
+ text="hello",
280
+ context_id="ctx-fast",
281
+ task_id="task-fast",
282
+ return_immediately=None,
283
+ )
284
+ queue = EventQueue()
285
+ events, _elapsed = await _execute_and_drain(
286
+ executor, ctx, queue, timeout=5.0,
287
+ )
288
+
289
+ assert len(events) == 1
290
+ assert isinstance(events[0], Message), (
291
+ f"fast path should return Message, got {type(events[0]).__name__}"
292
+ )
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