utim-cli 2.3.0__tar.gz → 2.3.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.
Files changed (141) hide show
  1. {utim_cli-2.3.0 → utim_cli-2.3.1}/CHANGELOG.md +5 -0
  2. {utim_cli-2.3.0/utim_cli.egg-info → utim_cli-2.3.1}/PKG-INFO +1 -1
  3. {utim_cli-2.3.0 → utim_cli-2.3.1}/pyproject.toml +3 -2
  4. utim_cli-2.3.1/utim/__init__.py +76 -0
  5. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/context_pruner.py +32 -19
  8. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/orchestrator.py +211 -85
  9. utim_cli-2.3.1/utim_cli/sdk/__init__.py +52 -0
  10. utim_cli-2.3.1/utim_cli/sdk/config.py +41 -0
  11. utim_cli-2.3.1/utim_cli/sdk/context.py +67 -0
  12. utim_cli-2.3.1/utim_cli/sdk/events.py +166 -0
  13. utim_cli-2.3.1/utim_cli/sdk/handlers.py +60 -0
  14. utim_cli-2.3.1/utim_cli/sdk/session.py +333 -0
  15. utim_cli-2.3.1/utim_cli/sdk/sidecar.py +164 -0
  16. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/auth.py +46 -0
  17. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/db.py +47 -0
  18. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/docs.md +1 -0
  19. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/features.md +1 -0
  20. utim_cli-2.3.1/utim_cli/server/local_llm.py +39 -0
  21. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/micro_batcher.py +18 -12
  22. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/router.py +68 -36
  23. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/completion_routes.py +37 -31
  24. utim_cli-2.3.1/utim_cli/server/server.py +94 -0
  25. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/share.py +99 -99
  26. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/state.py +1 -0
  27. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/task_dispatcher.py +6 -5
  28. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tools.py +12 -0
  29. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/utim.py +28 -5
  30. {utim_cli-2.3.0 → utim_cli-2.3.1/utim_cli.egg-info}/PKG-INFO +1 -1
  31. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/SOURCES.txt +8 -0
  32. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/top_level.txt +1 -0
  33. utim_cli-2.3.0/utim_cli/server/local_llm.py +0 -380
  34. utim_cli-2.3.0/utim_cli/server/server.py +0 -60
  35. {utim_cli-2.3.0 → utim_cli-2.3.1}/LICENSE +0 -0
  36. {utim_cli-2.3.0 → utim_cli-2.3.1}/MANIFEST.in +0 -0
  37. {utim_cli-2.3.0 → utim_cli-2.3.1}/README.md +0 -0
  38. {utim_cli-2.3.0 → utim_cli-2.3.1}/setup.cfg +0 -0
  39. {utim_cli-2.3.0 → utim_cli-2.3.1}/setup.py +0 -0
  40. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_check_obj.py +0 -0
  41. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_find_mcp.py +0 -0
  42. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_find_plan.py +0 -0
  43. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_find_tool.py +0 -0
  44. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_patch_multimodal.py +0 -0
  45. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_test_regex.py +0 -0
  46. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/agent.py +0 -0
  47. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/ask_helper.py +0 -0
  48. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/auth.py +0 -0
  49. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/backup.py +0 -0
  50. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/billing.py +0 -0
  51. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/blender_agent.py +0 -0
  52. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/bootstrap.py +0 -0
  53. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/brain.py +0 -0
  54. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/client_utils.py +0 -0
  55. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/config.py +0 -0
  56. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/constants.py +0 -0
  57. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/doctor.py +0 -0
  58. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/harbor.py +0 -0
  59. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/knowledge_graph.py +0 -0
  60. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/local_db.py +0 -0
  61. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/logger.py +0 -0
  62. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/mcp_clean_wrapper.py +0 -0
  63. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/mcp_client.py +0 -0
  64. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/mcp_registry.json +0 -0
  65. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/models.txt +0 -0
  66. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/reflection.py +0 -0
  67. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/report.py +0 -0
  68. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/scrapy_search.py +0 -0
  69. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/__init__.py +0 -0
  70. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/admin_auth.py +0 -0
  71. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/attribution.py +0 -0
  72. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/audit_log.py +0 -0
  73. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/batch_processor.py +0 -0
  74. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/captcha.py +0 -0
  75. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/cli_auth.py +0 -0
  76. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  77. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/about.md +0 -0
  78. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/changelog.md +0 -0
  79. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/license.md +0 -0
  80. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/pricing.md +0 -0
  81. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/privacy.md +0 -0
  82. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/refund.md +0 -0
  83. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/support.md +0 -0
  84. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/terms.md +0 -0
  85. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/email_utils.py +0 -0
  86. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/exchange_rate.py +0 -0
  87. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/firebase.py +0 -0
  88. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/fix_duplicate_users.py +0 -0
  89. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/history.py +0 -0
  90. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/logging_config.py +0 -0
  91. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/model_agent.py +0 -0
  92. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/models.py +0 -0
  93. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/pricing_updater.py +0 -0
  94. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/provision_build.py +0 -0
  95. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/rate_limit.py +0 -0
  96. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/rewards_engine.py +0 -0
  97. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/__init__.py +0 -0
  98. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/auth_routes.py +0 -0
  99. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/billing_routes.py +0 -0
  100. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/credit_routes.py +0 -0
  101. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/feedback_routes.py +0 -0
  102. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/marketplace_routes.py +0 -0
  103. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/quota_routes.py +0 -0
  104. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/quota_share_routes.py +0 -0
  105. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/referral_routes.py +0 -0
  106. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/rewards_routes.py +0 -0
  107. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/security_routes.py +0 -0
  108. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/session_routes.py +0 -0
  109. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/share_routes.py +0 -0
  110. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/storage_nodes.py +0 -0
  111. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/share_tui.py +0 -0
  112. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/situational_scoring.py +0 -0
  113. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/subagent_manager.py +0 -0
  114. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/__init__.py +0 -0
  115. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/feedback_dialog.py +0 -0
  116. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/history_dialog.py +0 -0
  117. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/marketplace_app_state.py +0 -0
  118. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/marketplace_dialog.py +0 -0
  119. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  120. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/mcp_dialog.py +0 -0
  121. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/miniagents_dialog.py +0 -0
  122. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/model_dialog.py +0 -0
  123. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/publish_dialog.py +0 -0
  124. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/quota_dialog.py +0 -0
  125. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  126. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/quota_share_dialog.py +0 -0
  127. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/resume_dialog.py +0 -0
  128. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/rewards_tui.py +0 -0
  129. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/skills_dialog.py +0 -0
  130. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/subagents_dialog.py +0 -0
  131. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/thinking_display.py +0 -0
  132. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/tools_dialog.py +0 -0
  133. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/update_dialog.py +0 -0
  134. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/utilities.py +0 -0
  135. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/utimmodel.txt +0 -0
  136. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/vector_memory.py +0 -0
  137. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/wheel.py +0 -0
  138. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/workspace.py +0 -0
  139. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/dependency_links.txt +0 -0
  140. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/entry_points.txt +0 -0
  141. {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/requires.txt +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.1] - 2026-08-19
4
+
5
+ ### 🛠️ Fixes & Enhancements
6
+ - Major bug fixes in agent loop and enhancements
7
+
3
8
  ## [2.3.0] - 2026-08-18
4
9
 
5
10
  ### 🚀 Features & Enhancements
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: Emend AI Proprietary EULA
6
6
  Project-URL: Homepage, https://utim.dev
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.3.0"
7
+ version = "2.3.1"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -93,7 +93,7 @@ utimlite = "utim_cli.utim:main_cli_entry"
93
93
 
94
94
 
95
95
  [tool.setuptools.packages.find]
96
- include = ["utim_cli*"]
96
+ include = ["utim*", "utim_cli*"]
97
97
  exclude = ["tests*", "landing*", "scripts*"]
98
98
 
99
99
  [tool.setuptools.package-data]
@@ -101,6 +101,7 @@ utim_cli = ["mcp_registry.json", "_version.py", "utimmodel.txt", "models.txt", "
101
101
 
102
102
 
103
103
  [tool.pytest.ini_options]
104
+ addopts = "-m 'not benchmark and not slow and not integration' --ignore=tests/test_benchmark.py --ignore=tests/bench_agent.py"
104
105
  markers = [
105
106
  "smoke: fast install validation tests (no live credentials required)",
106
107
  "slow: tests that make network calls or take more than 10 seconds",
@@ -0,0 +1,76 @@
1
+ """
2
+ UTIM Developer SDK
3
+ ------------------
4
+ Unified autonomous coding agent SDK.
5
+
6
+ Quickstart:
7
+ import utim
8
+
9
+ session = utim.Session()
10
+ async for event in session.send_message("Explain this codebase"):
11
+ if event.type == utim.EventType.TOKEN:
12
+ print(event.text, end="", flush=True)
13
+ """
14
+ try:
15
+ from utim_cli._version import VERSION as __version__
16
+ except ImportError:
17
+ __version__ = "2.3.0"
18
+
19
+ # Developer-friendly core classes (similar to `import genai` / `import openai`)
20
+ from utim_cli.sdk.session import UtimSession as Session
21
+ from utim_cli.sdk.config import UtimConfig as Config
22
+ from utim_cli.sdk.context import IDEContext, Diagnostic
23
+ from utim_cli.sdk.events import (
24
+ AgentEvent,
25
+ UtimEventType as EventType,
26
+ TokenStreamEvent,
27
+ ThoughtStreamEvent,
28
+ ToolCallStartEvent,
29
+ ToolCallEndEvent,
30
+ FileEditEvent,
31
+ CommandExecEvent,
32
+ ConfirmationRequestEvent,
33
+ PlanUpdateEvent,
34
+ SubagentEvent,
35
+ TurnEndEvent,
36
+ )
37
+ from utim_cli.sdk.handlers import (
38
+ ConfirmationHandler,
39
+ AutoApprovalHandler,
40
+ CallbackApprovalHandler,
41
+ )
42
+ from utim_cli.sdk.sidecar import UtimSidecarServer as SidecarServer
43
+
44
+ # Backwards compatible alias names
45
+ UtimSession = Session
46
+ UtimConfig = Config
47
+ UtimEventType = EventType
48
+ UtimSidecarServer = SidecarServer
49
+
50
+ __all__ = [
51
+ "__version__",
52
+ "Session",
53
+ "Config",
54
+ "IDEContext",
55
+ "Diagnostic",
56
+ "EventType",
57
+ "AgentEvent",
58
+ "TokenStreamEvent",
59
+ "ThoughtStreamEvent",
60
+ "ToolCallStartEvent",
61
+ "ToolCallEndEvent",
62
+ "FileEditEvent",
63
+ "CommandExecEvent",
64
+ "ConfirmationRequestEvent",
65
+ "PlanUpdateEvent",
66
+ "SubagentEvent",
67
+ "TurnEndEvent",
68
+ "ConfirmationHandler",
69
+ "AutoApprovalHandler",
70
+ "CallbackApprovalHandler",
71
+ "SidecarServer",
72
+ "UtimSession",
73
+ "UtimConfig",
74
+ "UtimEventType",
75
+ "UtimSidecarServer",
76
+ ]
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "2.3.0"
8
+ __version__ = "2.3.1"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.3.0"
14
+ VERSION = "2.3.1"
15
15
 
16
16
 
@@ -102,27 +102,40 @@ def _strip_thinking_blocks(messages: List[Dict], keep_last: int = 1) -> List[Dic
102
102
  return result
103
103
 
104
104
 
105
- def _deduplicate_tool_outputs(messages: List[Dict]) -> List[Dict]:
106
- """Compress repeated identical tool responses to a one-liner stub.
105
+ def _deduplicate_tool_outputs(messages: List[Dict], keep_last_tools: int = 5) -> List[Dict]:
106
+ """Compress repeated identical tool responses to a one-liner stub,
107
+ WHILE STRICTLY PRESERVING the most recent `keep_last_tools` tool outputs 100% verbatim.
107
108
 
108
- When the agent calls the same tool with the same arguments repeatedly and gets
109
- the same output (e.g. re-reading an unchanged file multiple times), only the
110
- first occurrence is kept verbatim. Subsequent duplicates are replaced with:
111
- [Duplicate tool output — identical to earlier result, omitted to save tokens]
109
+ The last 5 tool outputs must NEVER be deduplicated, truncated, or stubbed out,
110
+ even if they are duplicates of earlier reads, because the model requires the exact
111
+ code lines and context to execute precise file edits and tool actions.
112
112
  """
113
- # Map: (tool_call_id_of_preceding_assistant_call -> output_hash)
114
- # We key on (tool_name, args_hash) -> first_output_hash seen
115
- seen: dict = {} # (tool_name_or_id, content_hash) -> True
113
+ # Identify indices of all tool messages
114
+ tool_msg_indices = [i for i, m in enumerate(messages) if m.get("role") == "tool"]
115
+ exempt_indices = set(tool_msg_indices[-keep_last_tools:]) if len(tool_msg_indices) >= 1 else set()
116
+
117
+ seen: dict = {} # (tool_name, content_hash) -> True
116
118
  result = []
117
119
 
118
- for msg in messages:
120
+ for i, msg in enumerate(messages):
119
121
  if msg.get("role") == "tool":
120
122
  content = msg.get("content") or ""
121
123
  tool_name = msg.get("name", "")
124
+
125
+ # CRITICAL: Never deduplicate or stub any of the last 5 tool calls!
126
+ if i in exempt_indices:
127
+ result.append(msg)
128
+ continue
129
+
130
+ # Never deduplicate tool error messages
131
+ if isinstance(content, str) and any(err in content.lower() for err in ["error", "exception", "failed", "failure", "traceback", "syntaxerror", "not found"]):
132
+ result.append(msg)
133
+ continue
134
+
122
135
  # Use first 300 chars of content for the hash to avoid hashing huge outputs
123
136
  content_key = (tool_name, hashlib.md5(content[:300].encode(errors="replace")).hexdigest())
124
137
  if content_key in seen and len(content) > 200:
125
- # Replace with stub
138
+ # Replace older duplicate with stub
126
139
  msg = dict(msg)
127
140
  msg["content"] = (
128
141
  f"[Duplicate {tool_name!r} output — identical to earlier result, "
@@ -320,13 +333,13 @@ def resolve_model_context_envelope(model_id: str, raw_context_window: int = 0) -
320
333
  }
321
334
 
322
335
 
323
- def sanitize_message_sequence(messages: List[Dict]) -> List[Dict]:
336
+ def sanitize_message_sequence(messages: List[Dict], keep_last_tools: int = 5) -> List[Dict]:
324
337
  """
325
338
  Ensure the messages list is valid for LLM API calls.
326
339
  - Each tool message must have a preceding assistant message with the matching tool_call_id.
327
340
  - Each assistant message with tool_calls must have corresponding tool messages.
328
341
  - Strips <think> blocks from non-current assistant messages to save tokens.
329
- - Deduplicates repeated identical tool outputs.
342
+ - Deduplicates older repeated identical tool outputs (strictly preserving the last 5 tool calls verbatim).
330
343
  """
331
344
  if not messages:
332
345
  return messages
@@ -334,8 +347,8 @@ def sanitize_message_sequence(messages: List[Dict]) -> List[Dict]:
334
347
  # Strip thinking blocks from older assistant messages
335
348
  messages = _strip_thinking_blocks(messages)
336
349
 
337
- # Compress duplicate tool outputs
338
- messages = _deduplicate_tool_outputs(messages)
350
+ # Compress older duplicate tool outputs while protecting last 5 tool calls
351
+ messages = _deduplicate_tool_outputs(messages, keep_last_tools=keep_last_tools)
339
352
 
340
353
  # Step 1: Identify all tool call IDs present in tool messages
341
354
  present_tool_ids = {
@@ -387,7 +400,7 @@ def sanitize_message_sequence(messages: List[Dict]) -> List[Dict]:
387
400
  def soft_prune_messages(
388
401
  messages: List[Dict],
389
402
  turn_msg_start: int = 1,
390
- keep_last_tools: int = 4,
403
+ keep_last_tools: int = 5,
391
404
  max_char_threshold: int = 1200,
392
405
  ) -> List[Dict]:
393
406
  """
@@ -395,7 +408,7 @@ def soft_prune_messages(
395
408
 
396
409
  Balanced policy ('not too aggressive, not too light'):
397
410
  - Anchors: System prompt (idx 0) and the root User prompt (turn_msg_start) are strictly preserved.
398
- - Active Working Tail: The most recent `keep_last_tools` tool results & their assistant calls are kept 100% verbatim.
411
+ - Active Working Tail: The most recent `keep_last_tools` (at least 5) tool results & their assistant calls are kept 100% verbatim.
399
412
  - Errors / Failures: Any tool output containing error keywords (e.g. 'error', 'exception', 'failed', 'traceback', 'syntaxerror') is preserved verbatim to prevent debugging amnesia.
400
413
  - Read / Search Outputs (older than tail): If longer than `max_char_threshold`, softly truncates the middle (keeps head 350 chars + tail 200 chars) with a clean diagnostic placeholder.
401
414
  - Successful Mutation / Command Outputs: If verbose, compacted to clean status indicators without losing file targets.
@@ -417,7 +430,7 @@ def soft_prune_messages(
417
430
 
418
431
  # If not enough tool messages have accumulated in this turn, just sanitize and return
419
432
  if len(tool_indices) <= keep_last_tools:
420
- return sanitize_message_sequence(cleaned_messages)
433
+ return sanitize_message_sequence(cleaned_messages, keep_last_tools=keep_last_tools)
421
434
 
422
435
  # Older tool message indices that are eligible for soft pruning
423
436
  eligible_tool_indices = set(tool_indices[:-keep_last_tools])
@@ -523,7 +536,7 @@ def soft_prune_messages(
523
536
 
524
537
  result_messages.append(msg)
525
538
 
526
- return sanitize_message_sequence(result_messages)
539
+ return sanitize_message_sequence(result_messages, keep_last_tools=keep_last_tools)
527
540
 
528
541
 
529
542
  def score_message_importance(message: Dict, llm_key: str = None) -> float:
@@ -893,6 +893,104 @@ def _lesson_relevant_to_task(lesson_line: str, active_task: str) -> bool:
893
893
  return True
894
894
 
895
895
 
896
+ def _clean_summary_text(text: str, max_len: int = 100) -> str:
897
+ """Clean terminal box-drawing characters, ANSI escapes, and multi-line clutter for summaries."""
898
+ if not text:
899
+ return ""
900
+ # Strip ANSI escape sequences
901
+ text = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", text)
902
+ # Strip box-drawing and decorative border characters
903
+ box_chars = "╭╮╯╰─│┌┐└┘├┤┬┴┼═║╒╓╔╕╖╗╘╙╚╛╜╝▀▄█▌▐░▒▓■□▲▼◆◇○●❯❮"
904
+ for ch in box_chars:
905
+ text = text.replace(ch, " ")
906
+
907
+ # Filter out empty lines or pure duration/border strings
908
+ clean_lines = []
909
+ for line in text.splitlines():
910
+ line = line.strip()
911
+ if not line:
912
+ continue
913
+ # Skip pure timestamp/duration lines (e.g. "4m 49s", "12s")
914
+ if re.match(r"^\d+[mshd]\s*\d*[mshd]?$", line):
915
+ continue
916
+ clean_lines.append(line)
917
+
918
+ if not clean_lines:
919
+ return ""
920
+
921
+ first_line = clean_lines[0]
922
+ # Collapse multiple whitespace
923
+ first_line = re.sub(r"\s+", " ", first_line).strip()
924
+ if len(first_line) > max_len:
925
+ first_line = first_line[:max_len].rstrip() + "..."
926
+ return first_line
927
+
928
+
929
+ def _format_session_history_summary(turn_history: Optional[List[Dict[str, Any]]], max_turns: int = 5) -> str:
930
+ """Generate a clean, structured, non-truncated summary of recent completed turns."""
931
+ if not turn_history:
932
+ return ""
933
+ # Only consider completed turns (ignore in-progress turns and background system events)
934
+ completed_turns = [
935
+ t for t in turn_history
936
+ if t.get("user_msg") and not t.get("is_in_progress") and not t.get("is_system_event")
937
+ ]
938
+ if not completed_turns:
939
+ return ""
940
+
941
+ recent_turns = completed_turns[-max_turns:]
942
+ summary_lines = []
943
+ for idx, t in enumerate(recent_turns, 1):
944
+ raw_msg = t.get("user_msg", "")
945
+ req_clean = _clean_summary_text(raw_msg, max_len=100)
946
+ if not req_clean:
947
+ continue
948
+
949
+ changes = t.get("changes", [])
950
+ changed_names = list({
951
+ os.path.basename(c.get("file_path", "") or c.get("path", ""))
952
+ for c in changes
953
+ if c.get("file_path") or c.get("path")
954
+ })
955
+
956
+ status = "Aborted by user" if t.get("aborted") else "Completed"
957
+
958
+ # Check if there are tool actions or messages
959
+ msgs = t.get("messages", [])
960
+ last_assistant_msg = ""
961
+ tools_used = []
962
+ for m in msgs:
963
+ if m.get("role") == "assistant":
964
+ if m.get("tool_calls"):
965
+ for tc in m["tool_calls"]:
966
+ fn = tc.get("function", {}).get("name")
967
+ if fn and fn not in tools_used:
968
+ tools_used.append(fn)
969
+ elif m.get("content"):
970
+ last_assistant_msg = m.get("content")
971
+
972
+ action_parts = []
973
+ if changed_names:
974
+ action_parts.append(f"modified {', '.join(f'`{n}`' for n in changed_names[:4])}")
975
+ elif tools_used:
976
+ action_parts.append(f"used {', '.join(f'`{t}`' for t in tools_used[:3])}")
977
+
978
+ action_summary = f" ({', '.join(action_parts)})" if action_parts else ""
979
+
980
+ # Outcome summary
981
+ outcome = ""
982
+ if last_assistant_msg:
983
+ clean_ast = _clean_summary_text(last_assistant_msg, max_len=80)
984
+ if clean_ast and clean_ast != req_clean:
985
+ outcome = f" → Outcome: {clean_ast}"
986
+
987
+ summary_lines.append(f"- Step {idx}: \"{req_clean}\" [{status}]{action_summary}{outcome}")
988
+
989
+ if not summary_lines:
990
+ return ""
991
+ return "\n".join(summary_lines)
992
+
993
+
896
994
  def get_system_prompt(user_prompt: str = "", current_iteration: int = 0, elapsed_seconds: int = 0, turn_history: Optional[List[Dict]] = None, messages: Optional[List[Dict]] = None) -> str:
897
995
  """Gets the dynamic system prompt with active MCP servers and semantically fetched Hugging Face vector memories."""
898
996
  # If the user prompt is just a casual greeting/message, return a minimal prompt to save tokens
@@ -1070,16 +1168,45 @@ def get_system_prompt(user_prompt: str = "", current_iteration: int = 0, elapsed
1070
1168
  except Exception:
1071
1169
  pass
1072
1170
 
1073
- # 4. Situational Active Skills Scoring & Injection
1171
+ # ── 4. Inject Structured Session Summary Memory ──
1172
+ session_summary = _format_session_history_summary(turn_history, max_turns=5)
1173
+ session_prompt = ""
1174
+ if session_summary:
1175
+ session_prompt = f"\n\n### SESSION HISTORY (Previous Steps Completed in this Session) ###\n{session_summary}\n"
1176
+
1177
+ # ── 5. Situational Active Skills Scoring & Injection (Analyzed from Active Summarized Context) ──
1074
1178
  skills_prompt = ""
1075
1179
  try:
1076
1180
  from utim_cli.bootstrap import scan_available_skills, rank_relevant_skills
1077
1181
  skills = scan_available_skills()
1078
1182
  if skills:
1183
+ # Build comprehensive active summarized context query:
1184
+ # Combines user prompt, derived ongoing task, completed turn summaries, and modified files
1185
+ context_query_parts = []
1186
+ if user_prompt and user_prompt.strip():
1187
+ context_query_parts.append(user_prompt.strip())
1188
+
1079
1189
  active_task = _get_active_task(user_prompt, messages=messages) if user_prompt else ""
1080
-
1081
- # Rank skills using BM25, IDF, and Domain Intent scoring (threshold 14.0)
1082
- ranked_skills = rank_relevant_skills(active_task or user_prompt or "", skills=skills, min_score=14.0, max_results=4)
1190
+ if active_task and active_task != user_prompt:
1191
+ context_query_parts.append(active_task)
1192
+
1193
+ if session_summary:
1194
+ context_query_parts.append(session_summary)
1195
+
1196
+ # Include recent touched files from turn_history to capture architectural domain
1197
+ if turn_history:
1198
+ recent_changes = []
1199
+ for t in turn_history[-3:]:
1200
+ for ch in t.get("changes", []):
1201
+ if isinstance(ch, dict) and ch.get("path"):
1202
+ recent_changes.append(ch.get("path"))
1203
+ if recent_changes:
1204
+ context_query_parts.append("Files: " + " ".join(recent_changes[-6:]))
1205
+
1206
+ active_summarized_context = "\n".join(context_query_parts).strip()
1207
+
1208
+ # Rank skills using BM25, IDF, and Domain Intent scoring on active summarized context (threshold 14.0)
1209
+ ranked_skills = rank_relevant_skills(active_summarized_context or user_prompt or "", skills=skills, min_score=14.0, max_results=4)
1083
1210
  relevant_skills = [name for name, _ in ranked_skills]
1084
1211
 
1085
1212
  # Filter out already read/seen skills
@@ -1135,36 +1262,6 @@ def get_system_prompt(user_prompt: str = "", current_iteration: int = 0, elapsed
1135
1262
  )
1136
1263
  except Exception:
1137
1264
  pass
1138
-
1139
- # ── 5. Inject Structured Session Summary Memory ──
1140
- session_summary = ""
1141
- import os
1142
- from utim_cli.config import get_utim_dir
1143
- summary_path = get_utim_dir() / "session_summary.md"
1144
- if os.path.exists(summary_path):
1145
- try:
1146
- with open(summary_path, "r", encoding="utf-8") as sf:
1147
- raw_summary = sf.read().strip()
1148
- # Take only the last 3 lines to prevent checklist bloat
1149
- session_summary = "\n".join(raw_summary.splitlines()[-3:])
1150
- except Exception:
1151
- pass
1152
-
1153
- if not session_summary and turn_history:
1154
- # Generate a LEAN summary: only last 8 turns, one-line per request + changed files
1155
- recent_turns = turn_history[-8:]
1156
- summary_lines = []
1157
- for idx, t in enumerate(recent_turns):
1158
- req = t.get("user_msg", "").strip().split("\n")[0][:80]
1159
- changes = t.get("changes", [])
1160
- changed_names = list({os.path.basename(c.get("file_path", "") or c.get("path", "")) for c in changes if c.get("file_path") or c.get("path")})
1161
- files_str = ", ".join(f"`{n}`" for n in changed_names[:5]) if changed_names else "—"
1162
- summary_lines.append(f"- Turn {idx+1}: {req} → {files_str}")
1163
- session_summary = "\n".join(summary_lines)
1164
-
1165
- session_prompt = ""
1166
- if session_summary:
1167
- session_prompt = f"\n\n### SESSION HISTORY ###\n{session_summary}\n"
1168
1265
  # ── 6. Inject Live User Hint Directives (System Prompt Override) ──
1169
1266
  hint_prompt = ""
1170
1267
  try:
@@ -1539,16 +1636,19 @@ class Orchestrator:
1539
1636
  "",
1540
1637
  "## Completed Actions",
1541
1638
  ]
1542
- if not clean_turn_history:
1639
+ # Only include finished user actions in the completed actions log
1640
+ completed_actions = [t for t in clean_turn_history if not t.get("is_in_progress") and not t.get("is_system_event")]
1641
+ if not completed_actions:
1543
1642
  summary_lines.append("*No actions completed yet in this session.*")
1544
1643
  else:
1545
- for idx, t in enumerate(clean_turn_history):
1644
+ for idx, t in enumerate(completed_actions):
1546
1645
  req = t.get("user_msg", "").strip()
1547
- req_short = req.split("\n")[0][:120]
1548
- if len(req.split("\n")) > 1 or len(req) > 120:
1549
- req_short += "..."
1646
+ req_short = _clean_summary_text(req, max_len=120)
1647
+ if not req_short:
1648
+ req_short = "Action"
1649
+ status = " [Aborted by user]" if t.get("aborted") else ""
1550
1650
 
1551
- summary_lines.append(f"### Step {idx+1}: {req_short}")
1651
+ summary_lines.append(f"### Step {idx+1}: {req_short}{status}")
1552
1652
 
1553
1653
  # Add files modified
1554
1654
  changes = t.get("changes", [])
@@ -4740,30 +4840,48 @@ class Orchestrator:
4740
4840
  joined_context = joined_context[:SUBAGENT_CONTEXT_BUDGET_CHARS] + "\n... [context budget reached] ..."
4741
4841
  active_context = joined_context
4742
4842
  else:
4743
- # Main Agent Checklist
4744
- active_context = "\n\n### ACTIVE CONTEXT CHECKLIST:"
4843
+ # Main Agent Active Context Checklist (3000 Token Budget / ~12000 Chars)
4844
+ MAIN_AGENT_CONTEXT_BUDGET_CHARS = 12000
4845
+ main_items = [
4846
+ "\n\n### ACTIVE CONTEXT CHECKLIST (3000 Token Budget) ###",
4847
+ f"- **Execution Progress**: Iteration {task_iter + 1} ({task_elapsed}s elapsed)",
4848
+ ]
4745
4849
 
4746
- # A. Find active file path dynamically from recent messages
4747
- active_file = ""
4748
- for msg in reversed(messages_snapshot):
4850
+ # 1. Extract touched/inspected files dynamically across messages
4851
+ touched_files = []
4852
+ for msg in messages_snapshot:
4749
4853
  if msg.get("role") == "assistant" and msg.get("tool_calls"):
4750
4854
  for tc in msg["tool_calls"]:
4751
4855
  func = tc.get("function", {})
4752
- if func.get("name") in ("write_file", "edit_file", "read_file"):
4856
+ if func.get("name") in ("write_file", "edit_file", "read_file", "delete_file", "view_file", "replace_file_content"):
4753
4857
  try:
4754
- args = json.loads(func.get("arguments", "{}"))
4755
- active_file = args.get("filepath", args.get("path", ""))
4756
- if active_file:
4757
- break
4858
+ args = json.loads(func.get("arguments", "{}")) if isinstance(func.get("arguments"), str) else func.get("arguments", {})
4859
+ fp = args.get("filepath", args.get("path", args.get("AbsolutePath", args.get("TargetFile", ""))))
4860
+ if fp and fp not in touched_files:
4861
+ touched_files.append(fp)
4758
4862
  except Exception:
4759
4863
  pass
4760
- if active_file:
4761
- break
4762
- if active_file:
4763
- active_context += f"\n- **Current File**: {active_file}"
4864
+ if touched_files:
4865
+ main_items.append(f"- **Files Inspected/Modified**: {', '.join(f'`{f}`' for f in touched_files[-15:])}")
4866
+
4867
+ # 2. Recent Tool Action History (Last 8 actions with arguments & outputs)
4868
+ if hasattr(self, "tool_results") and self.tool_results:
4869
+ action_history = []
4870
+ recent_actions = self.tool_results[-8:]
4871
+ for idx, tr in enumerate(recent_actions):
4872
+ fname = tr.get("func_name", "tool")
4873
+ args_obj = tr.get("arguments", {})
4874
+ args_summary = ""
4875
+ if isinstance(args_obj, dict):
4876
+ args_summary = ", ".join(f"{k}={repr(v)[:50]}" for k, v in list(args_obj.items())[:3])
4877
+ elif isinstance(args_obj, str) and args_obj.strip():
4878
+ args_summary = args_obj[:80]
4879
+ fres = str(tr.get("result", ""))[:300].replace("\n", " ")
4880
+ action_history.append(f" {idx + 1}. `{fname}({args_summary})` → {fres}")
4881
+ if action_history:
4882
+ main_items.append("- **Recent Tool Actions**:\n" + "\n".join(action_history))
4764
4883
 
4765
- # B. Get latest command status from the most recent run_command output
4766
- last_command_output = ""
4884
+ # 3. Latest Command Output / Exit Codes
4767
4885
  for msg in reversed(messages_snapshot):
4768
4886
  if msg.get("role") == "tool" and msg.get("name") == "run_command":
4769
4887
  content = msg.get("content", "")
@@ -4771,49 +4889,53 @@ class Orchestrator:
4771
4889
  if lines:
4772
4890
  exit_code_line = next((l for l in lines if "exit_code:" in l), "")
4773
4891
  err_lines = [l for l in lines if "error" in l.lower() or "failed" in l.lower() or "exception" in l.lower()]
4774
- last_command_output = f"Command exit: {exit_code_line or 'unknown'}"
4892
+ cmd_summary = f"Exit: {exit_code_line or '0'}"
4775
4893
  if err_lines:
4776
- last_command_output += f" | Errors: {'; '.join(err_lines[:2])}"
4894
+ cmd_summary += f" | Errors: {'; '.join(err_lines[:3])}"
4777
4895
  else:
4778
- last_command_output += f" | Output: {'; '.join(lines[:2])}"
4896
+ cmd_summary += f" | Output: {'; '.join(lines[:3])}"
4897
+ main_items.append(f"- **Latest Shell Command**: {cmd_summary}")
4779
4898
  break
4780
- if last_command_output:
4781
- active_context += f"\n- **Latest Command Status**: {last_command_output}"
4782
4899
 
4783
- # C. Read active todos from todos.json
4784
- active_todo_checklist = ""
4900
+ # 4. Active Project Todos / Task Checklist (from todos.json)
4785
4901
  todo_file = ".utim_tmp/todos.json"
4786
4902
  if os.path.exists(todo_file):
4787
4903
  try:
4788
4904
  with open(todo_file, "r", encoding="utf-8") as f:
4789
4905
  todos = json.load(f)
4790
4906
  if todos and isinstance(todos, dict):
4791
- active_todo_checklist = "\n### ACTIVE TASK CHECKLIST:\n"
4792
- for tid, t in todos.items():
4907
+ todo_lines = []
4908
+ for tid, t in list(todos.items())[:15]:
4793
4909
  status_mark = "[x]" if t.get("status") == "done" else "[ ]"
4794
- active_todo_checklist += f"{status_mark} {t.get('description', '')}\n"
4910
+ todo_lines.append(f" {status_mark} {t.get('description', '')}")
4911
+ if todo_lines:
4912
+ main_items.append("- **Project Task Checklist**:\n" + "\n".join(todo_lines))
4795
4913
  except Exception:
4796
4914
  pass
4797
4915
 
4798
- if active_todo_checklist:
4799
- active_context += active_todo_checklist
4800
- else:
4801
- # BUG 7 FIX: Guard against effective_turn_start being out-of-bounds
4802
- # after _compress_intra_turn shortens self.messages. Without this
4803
- # guard the IndexError is silently swallowed and the model gets no
4804
- # active objective in its system prompt for the rest of the turn.
4805
- if 0 < effective_turn_start < len(messages_snapshot):
4806
- raw_content = messages_snapshot[effective_turn_start].get("content") or ""
4807
- if isinstance(raw_content, list):
4808
- text_parts = []
4809
- for part in raw_content:
4810
- if isinstance(part, dict) and part.get("type") == "text":
4811
- text_parts.append(part.get("text", ""))
4812
- text_str = " ".join(text_parts)
4813
- else:
4814
- text_str = str(raw_content)
4815
- obj = text_str[:600]
4816
- active_context += f"\n- **Active Objective**: {obj}..."
4916
+ # 5. Background Processes & Subagent Tasks
4917
+ try:
4918
+ from utim_cli.tools import _BACKGROUND_PROCESSES
4919
+ if _BACKGROUND_PROCESSES:
4920
+ bg_list = []
4921
+ for pid, proc in list(_BACKGROUND_PROCESSES.items())[:5]:
4922
+ status = "running" if proc.poll() is None else f"exit:{proc.returncode}"
4923
+ bg_list.append(f" • [{pid}] ({status})")
4924
+ if bg_list:
4925
+ main_items.append("- **Background Processes**:\n" + "\n".join(bg_list))
4926
+ except Exception:
4927
+ pass
4928
+
4929
+ # Multi-iteration deep loop context: if iteration > 0 and no todos, maintain high-level goal
4930
+ if task_iter > 0 and user_prompt and not any("Project Task Checklist" in item for item in main_items):
4931
+ clean_goal = _clean_summary_text(user_prompt, max_len=300)
4932
+ if clean_goal:
4933
+ main_items.append(f"- **Task In-Progress**: \"{clean_goal}\"")
4934
+
4935
+ joined_context = "\n".join(main_items)
4936
+ if len(joined_context) > MAIN_AGENT_CONTEXT_BUDGET_CHARS:
4937
+ joined_context = joined_context[:MAIN_AGENT_CONTEXT_BUDGET_CHARS] + "\n... [context budget reached] ..."
4938
+ active_context = joined_context
4817
4939
 
4818
4940
  system_msg["content"] += active_context
4819
4941
 
@@ -5499,6 +5621,7 @@ class Orchestrator:
5499
5621
  "messages": list(self.messages[turn_msg_start:]),
5500
5622
  "changes": [],
5501
5623
  "is_system_event": is_system_msg,
5624
+ "is_in_progress": True,
5502
5625
  })
5503
5626
  task_start_time = time.time()
5504
5627
 
@@ -6055,6 +6178,7 @@ class Orchestrator:
6055
6178
  "msg_end": len(self.messages),
6056
6179
  "messages": list(self.messages[turn_msg_start:]),
6057
6180
  "changes": list(self._turn_changes),
6181
+ "is_in_progress": True,
6058
6182
  })
6059
6183
 
6060
6184
  # Execute tools - use parallel execution for better performance
@@ -6594,6 +6718,7 @@ class Orchestrator:
6594
6718
  "msg_end": len(self.messages),
6595
6719
  "messages": list(self.messages[turn_msg_start:]),
6596
6720
  "changes": list(self._turn_changes),
6721
+ "is_in_progress": True,
6597
6722
  })
6598
6723
 
6599
6724
  tool_duration = time.time() - t_tool_start
@@ -6713,6 +6838,7 @@ class Orchestrator:
6713
6838
  "step_timings": list(self.turn_step_timings),
6714
6839
  "aborted": is_aborted,
6715
6840
  "is_system_event": is_system_msg,
6841
+ "is_in_progress": False,
6716
6842
  }
6717
6843
 
6718
6844
  self.turn_history.append(turn_entry)