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.
- {utim_cli-2.3.0 → utim_cli-2.3.1}/CHANGELOG.md +5 -0
- {utim_cli-2.3.0/utim_cli.egg-info → utim_cli-2.3.1}/PKG-INFO +1 -1
- {utim_cli-2.3.0 → utim_cli-2.3.1}/pyproject.toml +3 -2
- utim_cli-2.3.1/utim/__init__.py +76 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_version.py +1 -1
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/context_pruner.py +32 -19
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/orchestrator.py +211 -85
- utim_cli-2.3.1/utim_cli/sdk/__init__.py +52 -0
- utim_cli-2.3.1/utim_cli/sdk/config.py +41 -0
- utim_cli-2.3.1/utim_cli/sdk/context.py +67 -0
- utim_cli-2.3.1/utim_cli/sdk/events.py +166 -0
- utim_cli-2.3.1/utim_cli/sdk/handlers.py +60 -0
- utim_cli-2.3.1/utim_cli/sdk/session.py +333 -0
- utim_cli-2.3.1/utim_cli/sdk/sidecar.py +164 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/auth.py +46 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/db.py +47 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/docs.md +1 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/features.md +1 -0
- utim_cli-2.3.1/utim_cli/server/local_llm.py +39 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/micro_batcher.py +18 -12
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/router.py +68 -36
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/completion_routes.py +37 -31
- utim_cli-2.3.1/utim_cli/server/server.py +94 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/share.py +99 -99
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/state.py +1 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/task_dispatcher.py +6 -5
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tools.py +12 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/utim.py +28 -5
- {utim_cli-2.3.0 → utim_cli-2.3.1/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/SOURCES.txt +8 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/top_level.txt +1 -0
- utim_cli-2.3.0/utim_cli/server/local_llm.py +0 -380
- utim_cli-2.3.0/utim_cli/server/server.py +0 -60
- {utim_cli-2.3.0 → utim_cli-2.3.1}/LICENSE +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/MANIFEST.in +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/README.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/setup.cfg +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/setup.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_check_obj.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_find_mcp.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_find_plan.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_find_tool.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_patch_multimodal.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/_test_regex.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/config.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/constants.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/reflection.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/report.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/models.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.0 → utim_cli-2.3.1}/utim_cli.egg-info/requires.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
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.
|
|
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
|
+
]
|
|
@@ -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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
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(
|
|
1644
|
+
for idx, t in enumerate(completed_actions):
|
|
1546
1645
|
req = t.get("user_msg", "").strip()
|
|
1547
|
-
req_short = req
|
|
1548
|
-
if
|
|
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
|
-
|
|
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
|
-
#
|
|
4747
|
-
|
|
4748
|
-
for msg in
|
|
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
|
-
|
|
4756
|
-
if
|
|
4757
|
-
|
|
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
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
4892
|
+
cmd_summary = f"Exit: {exit_code_line or '0'}"
|
|
4775
4893
|
if err_lines:
|
|
4776
|
-
|
|
4894
|
+
cmd_summary += f" | Errors: {'; '.join(err_lines[:3])}"
|
|
4777
4895
|
else:
|
|
4778
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
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)
|