xpander-sdk 2.0.332__tar.gz → 2.0.334__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 (140) hide show
  1. {xpander_sdk-2.0.332/src/xpander_sdk.egg-info → xpander_sdk-2.0.334}/PKG-INFO +1 -1
  2. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/setup.py +1 -1
  3. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/__init__.py +1 -1
  4. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/context_optimizer.py +87 -2
  5. xpander_sdk-2.0.334/src/xpander_sdk/models/context_status.py +31 -0
  6. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/events.py +3 -0
  7. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/frameworks/agno.py +1 -1
  8. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334/src/xpander_sdk.egg-info}/PKG-INFO +1 -1
  9. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk.egg-info/SOURCES.txt +2 -0
  10. xpander_sdk-2.0.334/tests/test_context_optimizer_status_events.py +164 -0
  11. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/LICENSE +0 -0
  12. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/README.md +0 -0
  13. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/pyproject.toml +0 -0
  14. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/setup.cfg +0 -0
  15. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/consts/__init__.py +0 -0
  16. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/consts/api_routes.py +0 -0
  17. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/__init__.py +0 -0
  18. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/__init__.py +0 -0
  19. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/action_ledger.py +0 -0
  20. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/compact_retry_result.py +0 -0
  21. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/completion_evidence.py +0 -0
  22. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/constants.py +0 -0
  23. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/encryption.py +0 -0
  24. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/error_patterns.py +0 -0
  25. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/finalize_mode.py +0 -0
  26. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/helpers/__init__.py +0 -0
  27. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/helpers/chunking.py +0 -0
  28. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/helpers/recent_actions.py +0 -0
  29. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/helpers/secrets.py +0 -0
  30. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/helpers/tool_result.py +0 -0
  31. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/helpers/xml_safety.py +0 -0
  32. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/mixins/__init__.py +0 -0
  33. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/mixins/map_reduce.py +0 -0
  34. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/prompts.py +0 -0
  35. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/context_optimizer/workspace_cache.py +0 -0
  36. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/module_base.py +0 -0
  37. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/state.py +0 -0
  38. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/core/xpander_api_client.py +0 -0
  39. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/exceptions/__init__.py +0 -0
  40. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/exceptions/module_exception.py +0 -0
  41. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/__init__.py +0 -0
  42. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/action_ledger.py +0 -0
  43. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/activity.py +0 -0
  44. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/compactization.py +0 -0
  45. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/configuration.py +0 -0
  46. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/deep_planning.py +0 -0
  47. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/frameworks.py +0 -0
  48. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/generic.py +0 -0
  49. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/notifications.py +0 -0
  50. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/orchestrations.py +0 -0
  51. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/shared.py +0 -0
  52. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/models/user.py +0 -0
  53. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/__init__.py +0 -0
  54. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/__init__.py +0 -0
  55. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/agents_module.py +0 -0
  56. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/models/__init__.py +0 -0
  57. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/models/agent.py +0 -0
  58. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/models/agent_list.py +0 -0
  59. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/models/knowledge_bases.py +0 -0
  60. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/sub_modules/__init__.py +0 -0
  61. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/sub_modules/agent.py +0 -0
  62. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/utils/__init__.py +0 -0
  63. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/agents/utils/generic.py +0 -0
  64. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/__init__.py +0 -0
  65. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/backend_module.py +0 -0
  66. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/decorators/__init__.py +0 -0
  67. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/decorators/on_auth_event.py +0 -0
  68. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/events_registry.py +0 -0
  69. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/frameworks/__init__.py +0 -0
  70. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/frameworks/dispatch.py +0 -0
  71. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/utils/__init__.py +0 -0
  72. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/utils/extra_headers.py +0 -0
  73. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/utils/mcp_oauth.py +0 -0
  74. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/backend/utils/tool_call_events.py +0 -0
  75. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/__init__.py +0 -0
  76. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/decorators/__init__.py +0 -0
  77. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/decorators/on_boot.py +0 -0
  78. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/decorators/on_shutdown.py +0 -0
  79. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/decorators/on_task.py +0 -0
  80. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/decorators/on_tool.py +0 -0
  81. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/events_module.py +0 -0
  82. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/models/__init__.py +0 -0
  83. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/models/deployments.py +0 -0
  84. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/models/events.py +0 -0
  85. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/streaming_server.py +0 -0
  86. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/utils/__init__.py +0 -0
  87. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/utils/generic.py +0 -0
  88. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/events/utils/git_init.py +0 -0
  89. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/knowledge_bases/__init__.py +0 -0
  90. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/knowledge_bases/knowledge_bases_module.py +0 -0
  91. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/knowledge_bases/models/__init__.py +0 -0
  92. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/knowledge_bases/models/knowledge_bases.py +0 -0
  93. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/knowledge_bases/sub_modules/__init__.py +0 -0
  94. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/knowledge_bases/sub_modules/knowledge_base.py +0 -0
  95. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/knowledge_bases/sub_modules/knowledge_base_document_item.py +0 -0
  96. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/knowledge_bases/utils/__init__.py +0 -0
  97. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tasks/__init__.py +0 -0
  98. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tasks/models/__init__.py +0 -0
  99. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tasks/models/task.py +0 -0
  100. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tasks/models/tasks_list.py +0 -0
  101. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tasks/sub_modules/__init__.py +0 -0
  102. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tasks/sub_modules/task.py +0 -0
  103. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tasks/tasks_module.py +0 -0
  104. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tasks/utils/__init__.py +0 -0
  105. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tasks/utils/files.py +0 -0
  106. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/__init__.py +0 -0
  107. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/decorators/__init__.py +0 -0
  108. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/decorators/register_tool.py +0 -0
  109. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/models/__init__.py +0 -0
  110. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/models/mcp.py +0 -0
  111. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/models/tool_invocation_result.py +0 -0
  112. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/sub_modules/__init__.py +0 -0
  113. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/sub_modules/tool.py +0 -0
  114. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/tools_repository_module.py +0 -0
  115. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/utils/__init__.py +0 -0
  116. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/utils/generic.py +0 -0
  117. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/utils/local_tools.py +0 -0
  118. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/utils/schemas.py +0 -0
  119. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/modules/tools_repository/utils/workspace_payload.py +0 -0
  120. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/utils/__init__.py +0 -0
  121. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/utils/agents/__init__.py +0 -0
  122. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/utils/env.py +0 -0
  123. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/utils/event_loop.py +0 -0
  124. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/utils/generic.py +0 -0
  125. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk/utils/tools.py +0 -0
  126. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk.egg-info/dependency_links.txt +0 -0
  127. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk.egg-info/requires.txt +0 -0
  128. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/src/xpander_sdk.egg-info/top_level.txt +0 -0
  129. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_action_ledger_dedup.py +0 -0
  130. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_boot_shutdown_handlers.py +0 -0
  131. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_context_optimizer_chunked.py +0 -0
  132. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_context_optimizer_perf.py +0 -0
  133. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_context_optimizer_recent_actions.py +0 -0
  134. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_context_optimizer_unwrap.py +0 -0
  135. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_context_optimizer_window.py +0 -0
  136. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_context_window_detection.py +0 -0
  137. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_layer_1_cache_integration.py +0 -0
  138. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_reasoning_plan_task_id.py +0 -0
  139. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_tool_call_events.py +0 -0
  140. {xpander_sdk-2.0.332 → xpander_sdk-2.0.334}/tests/test_workspace_cache.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xpander-sdk
3
- Version: 2.0.332
3
+ Version: 2.0.334
4
4
  Summary: xpander.ai Backend-as-a-service for AI Agents - SDK
5
5
  Home-page: https://www.xpander.ai
6
6
  Author: xpanderAI
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="xpander-sdk",
8
- version="2.0.332",
8
+ version="2.0.334",
9
9
  author="xpanderAI",
10
10
  author_email="dev@xpander.ai",
11
11
  description="xpander.ai Backend-as-a-service for AI Agents - SDK",
@@ -15,7 +15,7 @@ Main Components:
15
15
  For more information, visit: https://xpander.ai
16
16
  """
17
17
 
18
- __version__ = "2.0.332"
18
+ __version__ = "2.0.334"
19
19
 
20
20
  # Backend-related imports
21
21
  from .modules.backend.backend_module import Backend
@@ -259,6 +259,11 @@ class XPanderContextOptimizer(MapReduceMixin, CompressionManager):
259
259
  default_factory=set, init=False, repr=False
260
260
  )
261
261
 
262
+ _last_emitted_status_pct: Optional[int] = field(
263
+ default=None, init=False, repr=False
264
+ )
265
+ _compacting: bool = field(default=False, init=False, repr=False)
266
+
262
267
  # In-memory cache + write queue for Layer 1 offloaded blobs. The optimizer
263
268
  # writes encrypted bytes here synchronously and the cache spawns the
264
269
  # actual workspace POST in the background; the agno tool hook short-
@@ -544,6 +549,8 @@ class XPanderContextOptimizer(MapReduceMixin, CompressionManager):
544
549
  # Layer 1: always runs — offload large tool results to workspace
545
550
  await self.layer_1_microcompact(messages)
546
551
 
552
+ await self._publish_context_status(messages)
553
+
547
554
  # Layer 3: agent-requested manual compaction (takes priority)
548
555
  if self.compact_requested:
549
556
  focus = self.compact_focus
@@ -1046,6 +1053,78 @@ class XPanderContextOptimizer(MapReduceMixin, CompressionManager):
1046
1053
  ),
1047
1054
  )
1048
1055
 
1056
+ async def apublish_context_status(
1057
+ self, messages: List[Message]
1058
+ ) -> "Optional[Any]":
1059
+ """Force-emit a context_status snapshot and return the payload; for mid-loop hooks (tool-call completions, sub-execution boundaries) where the caller wants to refresh the indicator without changing the compacting flag."""
1060
+ if not messages:
1061
+ return None
1062
+ return await self._publish_context_status(messages, force=True)
1063
+
1064
+ async def apublish_final_context_status(
1065
+ self, messages: List[Message]
1066
+ ) -> "Optional[Any]":
1067
+ """Force-emit a final context_status with compacting cleared; called at agno run end (RunCompleted/Cancelled/Error) so the indicator settles on post-final-message state."""
1068
+ if not messages:
1069
+ return None
1070
+ self._compacting = False
1071
+ return await self._publish_context_status(messages, force=True)
1072
+
1073
+ async def _publish_context_status(
1074
+ self,
1075
+ messages: List[Message],
1076
+ *,
1077
+ force: bool = False,
1078
+ ) -> "Optional[Any]":
1079
+ """Emit a context_status snapshot for the chat UI indicator; returns the payload (or None if unconfigured / estimate failed)."""
1080
+ if not self.agent or not self.task:
1081
+ return None
1082
+
1083
+ from xpander_sdk.models.context_status import ContextStatus
1084
+ from xpander_sdk.models.events import TaskUpdateEventType
1085
+
1086
+ try:
1087
+ estimated = self._estimate_tokens(messages)
1088
+ except Exception as exc:
1089
+ logger.debug(f"[context-optimizer] context_status estimate failed: {exc}")
1090
+ return None
1091
+
1092
+ window = max(1, int(self.context_window))
1093
+ pct = max(0.0, min(100.0, (estimated / window) * 100.0))
1094
+ self._last_emitted_status_pct = int(pct)
1095
+
1096
+ payload = ContextStatus(
1097
+ estimated_tokens=estimated,
1098
+ context_window=window,
1099
+ percent=round(pct, 1),
1100
+ auto_compact_threshold=self._auto_compact_threshold,
1101
+ emergency_threshold=self._emergency_compact_threshold,
1102
+ compacting=self._compacting,
1103
+ )
1104
+
1105
+ coro = self._push_activity_event(
1106
+ event_type=TaskUpdateEventType.ContextStatus,
1107
+ data=payload,
1108
+ )
1109
+ try:
1110
+ task = asyncio.create_task(coro)
1111
+ except RuntimeError as exc:
1112
+ logger.debug(f"[context-optimizer] context_status schedule failed: {exc}")
1113
+ coro.close()
1114
+ return payload
1115
+
1116
+ def _log_status_task_exception(t: "asyncio.Task[Any]") -> None:
1117
+ if t.cancelled():
1118
+ return
1119
+ exc = t.exception()
1120
+ if exc is not None:
1121
+ logger.debug(
1122
+ f"[context-optimizer] context_status publish failed: {exc}"
1123
+ )
1124
+
1125
+ task.add_done_callback(_log_status_task_exception)
1126
+ return payload
1127
+
1049
1128
  # Progress-emission rate-limiter state. Reset at the start of every
1050
1129
  # ``layer_2_auto_compact`` call by ``_reset_progress_state``.
1051
1130
  _progress_last_percent: float = field(default=-1.0, init=False, repr=False)
@@ -1379,6 +1458,9 @@ class XPanderContextOptimizer(MapReduceMixin, CompressionManager):
1379
1458
  f"threshold={self._auto_compact_threshold:,})"
1380
1459
  )
1381
1460
 
1461
+ self._compacting = True
1462
+ await self._publish_context_status(messages, force=True)
1463
+
1382
1464
  # Publish start event (fire-and-forget)
1383
1465
  await self._publish_compaction_start(
1384
1466
  trigger=trigger,
@@ -1574,6 +1656,9 @@ class XPanderContextOptimizer(MapReduceMixin, CompressionManager):
1574
1656
  reduce_phase_seconds=telemetry.get("reduce_phase_seconds"),
1575
1657
  )
1576
1658
 
1659
+ self._compacting = False
1660
+ await self._publish_context_status(messages, force=True)
1661
+
1577
1662
  ratio = (
1578
1663
  f"{(1 - post_tokens / pre_tokens) * 100:.1f}%"
1579
1664
  if pre_tokens > 0
@@ -1665,9 +1750,9 @@ class XPanderContextOptimizer(MapReduceMixin, CompressionManager):
1665
1750
  f"(trigger={trigger}, consecutive_failures={self._auto_compact_consecutive_failures}/"
1666
1751
  f"{MAX_CONSECUTIVE_COMPACT_FAILURES}): {exc}"
1667
1752
  )
1668
- # Publish error event (fire-and-forget)
1669
1753
  await self._publish_compaction_error(trigger=trigger, error=str(exc))
1670
- # Messages are NOT modified on failure — agent continues with full context
1754
+ self._compacting = False
1755
+ await self._publish_context_status(messages, force=True)
1671
1756
 
1672
1757
  # ================================================================== #
1673
1758
  # Pre-retry helpers
@@ -0,0 +1,31 @@
1
+ from pydantic import Field
2
+
3
+ from xpander_sdk.models.shared import XPanderSharedModel
4
+
5
+
6
+ class ContextStatus(XPanderSharedModel):
7
+ """Per-turn snapshot of the LLM context window for the chat UI indicator."""
8
+
9
+ estimated_tokens: int = Field(
10
+ ...,
11
+ description="Rough token count of the current message list (post-Layer-1 microcompaction).",
12
+ )
13
+ context_window: int = Field(
14
+ ..., description="Model context window size in tokens."
15
+ )
16
+ percent: float = Field(
17
+ ...,
18
+ description="estimated_tokens / context_window * 100, clamped to [0, 100].",
19
+ )
20
+ auto_compact_threshold: int = Field(
21
+ ...,
22
+ description="Token level at which Layer 2 auto-compaction fires.",
23
+ )
24
+ emergency_threshold: int = Field(
25
+ ...,
26
+ description="Token level at which the emergency safety net (88%) fires.",
27
+ )
28
+ compacting: bool = Field(
29
+ default=False,
30
+ description="True while a Layer 2 compaction is in flight on the current turn.",
31
+ )
@@ -91,6 +91,9 @@ class TaskUpdateEventType(str, Enum):
91
91
  # task compaction
92
92
  TaskCompactization = "task_compactization"
93
93
 
94
+ # context window status (per-turn snapshot, delta-gated)
95
+ ContextStatus = "context_status"
96
+
94
97
  # agent gateway
95
98
  AgentGatewayDecision = "agent_gateway_decision"
96
99
 
@@ -604,7 +604,7 @@ async def build_agent_args(
604
604
  {
605
605
  "name": xpander_agent.name,
606
606
  "model": model,
607
- "description": xpander_agent.instructions.description,
607
+ "description": xpander_agent.description if xpander_agent.description and xpander_agent.description is not None and len(xpander_agent.description) != 0 else xpander_agent.instructions.description,
608
608
  "instructions": (
609
609
  task.instructions_override
610
610
  if task.instructions_override
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xpander-sdk
3
- Version: 2.0.332
3
+ Version: 2.0.334
4
4
  Summary: xpander.ai Backend-as-a-service for AI Agents - SDK
5
5
  Home-page: https://www.xpander.ai
6
6
  Author: xpanderAI
@@ -40,6 +40,7 @@ src/xpander_sdk/models/action_ledger.py
40
40
  src/xpander_sdk/models/activity.py
41
41
  src/xpander_sdk/models/compactization.py
42
42
  src/xpander_sdk/models/configuration.py
43
+ src/xpander_sdk/models/context_status.py
43
44
  src/xpander_sdk/models/deep_planning.py
44
45
  src/xpander_sdk/models/events.py
45
46
  src/xpander_sdk/models/frameworks.py
@@ -127,6 +128,7 @@ tests/test_boot_shutdown_handlers.py
127
128
  tests/test_context_optimizer_chunked.py
128
129
  tests/test_context_optimizer_perf.py
129
130
  tests/test_context_optimizer_recent_actions.py
131
+ tests/test_context_optimizer_status_events.py
130
132
  tests/test_context_optimizer_unwrap.py
131
133
  tests/test_context_optimizer_window.py
132
134
  tests/test_context_window_detection.py
@@ -0,0 +1,164 @@
1
+ """Unit tests for the per-turn context_status event stream."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from types import SimpleNamespace
7
+ from unittest.mock import AsyncMock, MagicMock, patch
8
+
9
+ import pytest
10
+
11
+ from xpander_sdk.core.context_optimizer import context_optimizer as co
12
+ from xpander_sdk.core.context_optimizer.context_optimizer import (
13
+ XPanderContextOptimizer,
14
+ )
15
+ from xpander_sdk.models.events import TaskUpdateEventType
16
+
17
+
18
+ def _mk_msg(role: str = "user", content: str = "x"):
19
+ return SimpleNamespace(
20
+ role=role,
21
+ content=content,
22
+ tool_name=None,
23
+ tool_call_id=None,
24
+ to_dict=lambda r=role, c=content: {"role": r, "content": c},
25
+ )
26
+
27
+
28
+ def _make_optimizer(estimated: int = 50_000):
29
+ opt = XPanderContextOptimizer(
30
+ context_window=200_000,
31
+ reserved_for_output=20_000,
32
+ buffer_tokens=13_000,
33
+ chunked_compact_threshold=100_000,
34
+ )
35
+ opt.agent = SimpleNamespace(id="agent-1", configuration=MagicMock())
36
+ opt.task = SimpleNamespace(
37
+ id="task-1", organization_id="org-1", deep_planning=None
38
+ )
39
+ opt._push_activity_event = AsyncMock(return_value=None)
40
+ opt._estimate_tokens = MagicMock(return_value=estimated)
41
+ opt.layer_1_microcompact = AsyncMock(return_value=None)
42
+ return opt
43
+
44
+
45
+ def _status_calls(opt) -> list:
46
+ return [
47
+ call
48
+ for call in opt._push_activity_event.await_args_list
49
+ if call.kwargs.get("event_type") == TaskUpdateEventType.ContextStatus
50
+ ]
51
+
52
+
53
+ async def _drain_pending_tasks() -> None:
54
+ """Yield so detached create_task pushes run before asserting on AsyncMock awaits."""
55
+ for _ in range(5):
56
+ await asyncio.sleep(0)
57
+
58
+
59
+ @pytest.mark.asyncio
60
+ async def test_first_acompress_emits_one_status_event():
61
+ opt = _make_optimizer(estimated=50_000)
62
+ await opt.acompress(messages=[_mk_msg()])
63
+ await _drain_pending_tasks()
64
+
65
+ calls = _status_calls(opt)
66
+ assert len(calls) == 1
67
+ payload = calls[0].kwargs["data"]
68
+ assert payload.estimated_tokens == 50_000
69
+ assert payload.context_window == 200_000
70
+ assert payload.percent == 25.0
71
+ assert payload.compacting is False
72
+
73
+
74
+ @pytest.mark.asyncio
75
+ async def test_status_event_emits_every_acompress():
76
+ opt = _make_optimizer(estimated=50_000)
77
+ await opt.acompress(messages=[_mk_msg()])
78
+ opt._estimate_tokens.return_value = 50_500
79
+ await opt.acompress(messages=[_mk_msg()])
80
+ await _drain_pending_tasks()
81
+
82
+ assert len(_status_calls(opt)) == 2
83
+
84
+
85
+ @pytest.mark.asyncio
86
+ async def test_status_event_re_emitted_when_percent_bucket_changes():
87
+ opt = _make_optimizer(estimated=50_000)
88
+ await opt.acompress(messages=[_mk_msg()])
89
+ opt._estimate_tokens.return_value = 60_000
90
+ await opt.acompress(messages=[_mk_msg()])
91
+ await _drain_pending_tasks()
92
+
93
+ calls = _status_calls(opt)
94
+ assert len(calls) == 2
95
+ assert calls[1].kwargs["data"].percent == 30.0
96
+
97
+
98
+ @pytest.mark.asyncio
99
+ async def test_layer_2_brackets_with_compacting_true_then_false():
100
+ opt = _make_optimizer(estimated=200_000)
101
+
102
+ async def fake_call(
103
+ system_prompt,
104
+ user_prompt,
105
+ run_metrics=None,
106
+ progress_label="layer 2",
107
+ **kwargs,
108
+ ):
109
+ return "digest", 5, 3
110
+
111
+ with (
112
+ patch.object(co, "get_model", side_effect=lambda m: m),
113
+ patch.object(opt, "_run_llm_compaction_call", side_effect=fake_call),
114
+ ):
115
+ from agno.models.message import Message
116
+
117
+ opt.model = MagicMock(id="mock-model")
118
+ opt.model.get_provider = lambda: "mock"
119
+ messages = [
120
+ Message(role="system", content="sys"),
121
+ Message(role="user", content="u"),
122
+ ]
123
+ await opt.layer_2_auto_compact(messages=messages, trigger="auto")
124
+ await _drain_pending_tasks()
125
+
126
+ calls = _status_calls(opt)
127
+ assert len(calls) >= 2
128
+ assert calls[0].kwargs["data"].compacting is True
129
+ assert calls[-1].kwargs["data"].compacting is False
130
+
131
+
132
+ @pytest.mark.asyncio
133
+ async def test_layer_2_error_path_still_drops_compacting_flag():
134
+ opt = _make_optimizer(estimated=200_000)
135
+
136
+ async def failing_call(*a, **kw):
137
+ raise RuntimeError("provider exploded")
138
+
139
+ with (
140
+ patch.object(co, "get_model", side_effect=lambda m: m),
141
+ patch.object(opt, "_run_llm_compaction_call", side_effect=failing_call),
142
+ ):
143
+ from agno.models.message import Message
144
+
145
+ opt.model = MagicMock(id="mock-model")
146
+ opt.model.get_provider = lambda: "mock"
147
+ await opt.layer_2_auto_compact(
148
+ messages=[Message(role="system", content="sys")],
149
+ trigger="auto",
150
+ )
151
+ await _drain_pending_tasks()
152
+
153
+ calls = _status_calls(opt)
154
+ assert calls, "expected at least one context_status event"
155
+ assert calls[-1].kwargs["data"].compacting is False
156
+
157
+
158
+ @pytest.mark.asyncio
159
+ async def test_acompress_swallows_status_publish_failure():
160
+ opt = _make_optimizer(estimated=50_000)
161
+ opt._push_activity_event.side_effect = RuntimeError("redis down")
162
+
163
+ await opt.acompress(messages=[_mk_msg()])
164
+ await _drain_pending_tasks()
File without changes
File without changes
File without changes