mycode-coding-agent 0.1.0__py3-none-any.whl

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 (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,253 @@
1
+ """Thread-to-UI bridges for synchronous presentation interactions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from threading import Event, Lock
8
+ from typing import TYPE_CHECKING, Callable
9
+
10
+ from textual.message import Message
11
+
12
+ from mycode.mcp.trust import MCPTrustRequest, MCPTrustWarning
13
+ from mycode.permissions import ConfirmationRequest, ConfirmationResult
14
+ from mycode.subagents.contracts import SubAgentTask
15
+ from mycode.subagents.lifecycle import SubAgentStateTransition
16
+
17
+ if TYPE_CHECKING:
18
+ from mycode.subagents.runtime import SubAgentExecution
19
+
20
+
21
+ @dataclass(eq=False)
22
+ class MCPTrustResponseHandle:
23
+ request: MCPTrustRequest
24
+ _event: Event = field(default_factory=Event, init=False, repr=False)
25
+ _lock: Lock = field(default_factory=Lock, init=False, repr=False)
26
+ _approved: bool = field(default=False, init=False, repr=False)
27
+ _resolved: bool = field(default=False, init=False, repr=False)
28
+
29
+ def resolve(self, approved: bool) -> None:
30
+ with self._lock:
31
+ if self._resolved:
32
+ return
33
+ self._approved = approved
34
+ self._resolved = True
35
+ self._event.set()
36
+
37
+ def wait(self) -> bool:
38
+ self._event.wait()
39
+ return self._approved
40
+
41
+
42
+ class MCPTrustRequestMessage(Message):
43
+ def __init__(self, handle: MCPTrustResponseHandle) -> None:
44
+ self.handle = handle
45
+ super().__init__()
46
+
47
+
48
+ class MCPTrustWarningMessage(Message):
49
+ def __init__(self, warning: MCPTrustWarning) -> None:
50
+ self.warning = warning
51
+ super().__init__()
52
+
53
+
54
+ @dataclass(eq=False)
55
+ class PermissionResponseHandle:
56
+ request: ConfirmationRequest
57
+ _event: Event = field(default_factory=Event, init=False, repr=False)
58
+ _lock: Lock = field(default_factory=Lock, init=False, repr=False)
59
+ _result: ConfirmationResult = field(
60
+ default_factory=ConfirmationResult.rejected,
61
+ init=False,
62
+ repr=False,
63
+ )
64
+ _resolved: bool = field(default=False, init=False, repr=False)
65
+
66
+ def resolve(self, result: ConfirmationResult) -> None:
67
+ with self._lock:
68
+ if self._resolved:
69
+ return
70
+ self._result = result
71
+ self._resolved = True
72
+ self._event.set()
73
+
74
+ def wait(self) -> ConfirmationResult:
75
+ self._event.wait()
76
+ return self._result
77
+
78
+
79
+ class PermissionRequestMessage(Message):
80
+ def __init__(self, handle: PermissionResponseHandle) -> None:
81
+ self.handle = handle
82
+ super().__init__()
83
+
84
+
85
+ class TuiMCPTrustConfirmer:
86
+ """Adapt synchronous MCP trust confirmation to Textual messages."""
87
+
88
+ def __init__(self, post_message: Callable[[Message], bool]) -> None:
89
+ self._post_message = post_message
90
+ self._lock = Lock()
91
+ self._pending: set[MCPTrustResponseHandle] = set()
92
+ self._closed = False
93
+
94
+ def confirm(self, request: MCPTrustRequest) -> bool:
95
+ handle = MCPTrustResponseHandle(request)
96
+ with self._lock:
97
+ if self._closed:
98
+ return False
99
+ self._pending.add(handle)
100
+ try:
101
+ if not self._post_message(MCPTrustRequestMessage(handle)):
102
+ handle.resolve(False)
103
+ return handle.wait()
104
+ finally:
105
+ with self._lock:
106
+ self._pending.discard(handle)
107
+
108
+ def report_warning(self, warning: MCPTrustWarning) -> None:
109
+ self._post_message(MCPTrustWarningMessage(warning))
110
+
111
+ def reject_all(self) -> None:
112
+ with self._lock:
113
+ self._closed = True
114
+ pending = tuple(self._pending)
115
+ self._pending.clear()
116
+ for handle in pending:
117
+ handle.resolve(False)
118
+
119
+
120
+ class TuiConfirmer:
121
+ """Bridge synchronous Permission confirmation to the Textual UI."""
122
+
123
+ def __init__(self, post_message: Callable[[Message], bool]) -> None:
124
+ self._post_message = post_message
125
+ self._lock = Lock()
126
+ self._pending: set[PermissionResponseHandle] = set()
127
+ self._closed = False
128
+
129
+ def confirm(self, request: ConfirmationRequest) -> ConfirmationResult:
130
+ handle = PermissionResponseHandle(request)
131
+ with self._lock:
132
+ if self._closed:
133
+ return ConfirmationResult.rejected(
134
+ message="Permission confirmation unavailable.",
135
+ )
136
+ self._pending.add(handle)
137
+ try:
138
+ if not self._post_message(PermissionRequestMessage(handle)):
139
+ handle.resolve(
140
+ ConfirmationResult.rejected(
141
+ message="Permission confirmation unavailable.",
142
+ )
143
+ )
144
+ return handle.wait()
145
+ finally:
146
+ with self._lock:
147
+ self._pending.discard(handle)
148
+
149
+ def reject_all(self) -> None:
150
+ with self._lock:
151
+ self._closed = True
152
+ pending = tuple(self._pending)
153
+ self._pending.clear()
154
+ for handle in pending:
155
+ handle.resolve(
156
+ ConfirmationResult.rejected(
157
+ message="Permission confirmation unavailable.",
158
+ )
159
+ )
160
+
161
+
162
+ class SubAgentStateMessage(Message):
163
+ """Bridge a SubAgent state transition into the UI thread."""
164
+
165
+ def __init__(self, transition: SubAgentStateTransition) -> None:
166
+ self.transition = transition
167
+ super().__init__()
168
+
169
+
170
+ class SubAgentResultMessage(Message):
171
+ """Bridge a final SubAgent execution result into the UI thread."""
172
+
173
+ def __init__(
174
+ self,
175
+ task: SubAgentTask,
176
+ execution: SubAgentExecution,
177
+ ) -> None:
178
+ self.task = task
179
+ self.execution = execution
180
+ super().__init__()
181
+
182
+
183
+ class TuiSubAgentObserver:
184
+ """Bridge SubAgent lifecycle events into the Textual message queue.
185
+
186
+ Callbacks run on the SubAgent worker thread. They only post Textual
187
+ Messages; the UI thread owns every widget update. Snapshot and tool-audit
188
+ events are never rendered in the single-conversation timeline.
189
+ """
190
+
191
+ def __init__(self, post_message: Callable[[Message], bool]) -> None:
192
+ self._post_message = post_message
193
+ self._closed = False
194
+
195
+ def on_state(
196
+ self,
197
+ task: SubAgentTask,
198
+ transition: SubAgentStateTransition,
199
+ ) -> None:
200
+ del task
201
+ self._safe_post(SubAgentStateMessage(transition))
202
+
203
+ def on_snapshot(
204
+ self,
205
+ task: SubAgentTask,
206
+ run_id: str,
207
+ snapshot,
208
+ occurred_at: datetime,
209
+ ) -> None:
210
+ del task, run_id, snapshot, occurred_at
211
+
212
+ def on_tool_audit(
213
+ self,
214
+ task: SubAgentTask,
215
+ run_id: str,
216
+ audit,
217
+ occurred_at: datetime,
218
+ ) -> None:
219
+ del task, run_id, audit, occurred_at
220
+
221
+ def on_result(
222
+ self,
223
+ task: SubAgentTask,
224
+ execution: SubAgentExecution,
225
+ occurred_at: datetime,
226
+ ) -> None:
227
+ del occurred_at
228
+ self._safe_post(SubAgentResultMessage(task, execution))
229
+
230
+ def close(self) -> None:
231
+ self._closed = True
232
+
233
+ def _safe_post(self, message: Message) -> None:
234
+ if self._closed:
235
+ return
236
+ try:
237
+ self._post_message(message)
238
+ except Exception: # noqa: BLE001 - app may be closing; never raise into SubAgent thread
239
+ pass
240
+
241
+
242
+ __all__ = [
243
+ "MCPTrustRequestMessage",
244
+ "MCPTrustResponseHandle",
245
+ "MCPTrustWarningMessage",
246
+ "PermissionRequestMessage",
247
+ "PermissionResponseHandle",
248
+ "SubAgentResultMessage",
249
+ "SubAgentStateMessage",
250
+ "TuiConfirmer",
251
+ "TuiMCPTrustConfirmer",
252
+ "TuiSubAgentObserver",
253
+ ]
@@ -0,0 +1,266 @@
1
+ """Translate application and agent events into TUI view updates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from dataclasses import dataclass, field
7
+ from typing import Protocol
8
+
9
+ from mycode.agent.events import AgentEvent
10
+ from mycode.application.events import RuntimeEvent
11
+ from mycode.event_format import summarize_event_content, summarize_tool_arguments
12
+
13
+ SUBAGENT_TUI_SUMMARY_CHARS = 160
14
+
15
+
16
+ class TuiConversation(Protocol):
17
+ def add_user_message(self, content: str) -> None: ...
18
+
19
+ def append_assistant_delta(self, content: str) -> None: ...
20
+
21
+ def add_notice(self, content: str, *, level: str = "info") -> None: ...
22
+
23
+ def add_tool_activity(self, name: str, arguments: str) -> int: ...
24
+
25
+ def complete_tool_activity(
26
+ self,
27
+ token: int,
28
+ name: str,
29
+ *,
30
+ ok: bool,
31
+ summary: str,
32
+ ) -> None: ...
33
+
34
+
35
+ class TuiHeader(Protocol):
36
+ def set_session(self, value: str) -> None: ...
37
+
38
+
39
+ class TuiStatus(Protocol):
40
+ def set_status(self, value: str) -> None: ...
41
+
42
+
43
+ @dataclass
44
+ class TuiPresenter:
45
+ """Keep event interpretation out of Textual widgets.
46
+
47
+ The presenter accepts the existing RuntimeEvent and AgentEvent contracts.
48
+ It deliberately has no dependency on AgentRunner or AgentApplicationSession;
49
+ the TUI application owns startup and feeds the resulting events here.
50
+ """
51
+
52
+ conversation: TuiConversation
53
+ header: TuiHeader
54
+ status: TuiStatus
55
+ _pending_tool_rows: deque[tuple[int, str]] = field(
56
+ default_factory=deque,
57
+ init=False,
58
+ )
59
+
60
+ def show_user_message(self, content: str) -> None:
61
+ self.conversation.add_user_message(content)
62
+ self.status.set_status("UI Ready")
63
+
64
+ def present(self, event: RuntimeEvent) -> None:
65
+ if event.type == "runtime_ready":
66
+ self._present_runtime_ready(event)
67
+ return
68
+ if event.type == "mcp_status":
69
+ self._present_mcp_status(event)
70
+ return
71
+ if event.type == "agent" and event.agent_event is not None:
72
+ self.present_agent_event(event.agent_event)
73
+ return
74
+ if event.type == "turn_finished":
75
+ self._pending_tool_rows.clear()
76
+ self.status.set_status("Ready")
77
+
78
+ def present_agent_event(self, event: AgentEvent) -> None:
79
+ if event.type in {"reasoning_delta", "reasoning_state"}:
80
+ return
81
+
82
+ if event.type == "turn":
83
+ if event.turn_number is not None and event.max_turns is not None:
84
+ self.conversation.add_notice(
85
+ f"· turn {event.turn_number}/{event.max_turns}"
86
+ )
87
+ if event.content:
88
+ self.conversation.add_notice(
89
+ f"· turn notice {summarize_event_content(event.content)}"
90
+ )
91
+ return
92
+
93
+ if event.type == "context":
94
+ summary = summarize_event_content(event.content)
95
+ if summary:
96
+ self.conversation.add_notice(f"· context {summary}")
97
+ return
98
+
99
+ if event.type == "progress":
100
+ if event.progress is None:
101
+ return
102
+ progress = event.progress
103
+ self.conversation.add_notice(
104
+ "· progress "
105
+ f"reason={progress.reason} "
106
+ f"stagnation={progress.stagnation_turns} "
107
+ f"repeat={progress.same_tool_repeat} "
108
+ f"result_repeat={progress.same_result_repeat} "
109
+ f"resource_repeat={progress.resource_repeat}"
110
+ )
111
+ return
112
+
113
+ if event.type == "model_start":
114
+ self.status.set_status("Thinking…")
115
+ return
116
+
117
+ if event.type == "model_retry":
118
+ if event.model_retry is None:
119
+ return
120
+ retry = event.model_retry
121
+ self.status.set_status("Retrying…")
122
+ self.conversation.add_notice(
123
+ "⚠ model retry "
124
+ f"{retry.attempt}/{retry.max_retries} "
125
+ f"after {retry.delay_seconds:.1f}s "
126
+ f"error={retry.error_type}",
127
+ level="warning",
128
+ )
129
+ return
130
+
131
+ if event.type == "text_delta":
132
+ if event.content:
133
+ self.status.set_status("Responding…")
134
+ self.conversation.append_assistant_delta(event.content)
135
+ return
136
+
137
+ if event.type == "tool_call":
138
+ if event.tool_call is None:
139
+ return
140
+ tool_call = event.tool_call
141
+ arguments = summarize_tool_arguments(
142
+ tool_call.name,
143
+ tool_call.arguments,
144
+ )
145
+ token = self.conversation.add_tool_activity(
146
+ tool_call.name,
147
+ arguments,
148
+ )
149
+ self._pending_tool_rows.append((token, tool_call.name))
150
+ self.status.set_status("Running…")
151
+ return
152
+
153
+ if event.type == "tool_result":
154
+ if event.tool_result is None:
155
+ return
156
+ result = event.tool_result
157
+ if self._pending_tool_rows:
158
+ token, name = self._pending_tool_rows.popleft()
159
+ else:
160
+ metadata_name = result.metadata.get("tool_name")
161
+ name = metadata_name if isinstance(metadata_name, str) else "tool"
162
+ token = self.conversation.add_tool_activity(name, "")
163
+ summary = summarize_event_content(
164
+ result.content if result.ok else result.error
165
+ )
166
+ self.conversation.complete_tool_activity(
167
+ token,
168
+ name,
169
+ ok=result.ok,
170
+ summary=summary,
171
+ )
172
+ self.status.set_status("Running…" if not result.ok else "Thinking…")
173
+ return
174
+
175
+ if event.type == "artifact_warning":
176
+ summary = summarize_event_content(event.content)
177
+ if summary:
178
+ self.conversation.add_notice(f"⚠ {summary}", level="warning")
179
+ return
180
+
181
+ if event.type == "error":
182
+ summary = summarize_event_content(event.error or event.content)
183
+ self.conversation.add_notice(f"✗ {summary}", level="error")
184
+ self.status.set_status("Error")
185
+ return
186
+
187
+ if event.type == "stop":
188
+ reason = event.stop_reason or "unknown"
189
+ content = summarize_event_content(event.content)
190
+ message = f"· stop reason={reason}"
191
+ if content:
192
+ message += f" {content}"
193
+ self.conversation.add_notice(message)
194
+ self.status.set_status("Stopped")
195
+
196
+ def present_subagent_transition(self, transition) -> None:
197
+ """Render the few high-value SubAgent state transitions.
198
+
199
+ Completed and failed envelopes are shown once from on_result; the
200
+ intermediate transitions only annotate start, waiting for permission
201
+ and interruption (which may never reach on_result on an interrupt).
202
+ """
203
+ role = transition.role.capitalize()
204
+ if transition.state == "running" and transition.reason == "run_started":
205
+ self.conversation.add_notice(f"› {role} started")
206
+ return
207
+ if transition.state == "awaiting_confirmation":
208
+ self.conversation.add_notice(f"· {role} waiting for permission")
209
+ return
210
+ if transition.state == "interrupted":
211
+ self.conversation.add_notice(
212
+ f"⚠ {role} interrupted",
213
+ level="warning",
214
+ )
215
+
216
+ def present_subagent_result(self, execution) -> None:
217
+ result = execution.result
218
+ role = result.role.capitalize()
219
+ if result.status == "completed":
220
+ marker, level = "✓", "info"
221
+ elif result.status == "interrupted":
222
+ marker, level = "⚠", "warning"
223
+ else:
224
+ marker, level = "✗", "error"
225
+ line = f"{marker} {role} {result.status}"
226
+ summary = _subagent_summary(result.summary)
227
+ if summary:
228
+ line += f" {summary}"
229
+ self.conversation.add_notice(line, level=level)
230
+
231
+ def _present_runtime_ready(self, event: RuntimeEvent) -> None:
232
+ session = event.session_title or event.session_id or "—"
233
+ self.header.set_session(session)
234
+ self.conversation.add_notice(f"· runtime ready session={session}")
235
+ for warning in (*event.instruction_warnings, *event.skill_warnings):
236
+ summary = summarize_event_content(warning)
237
+ if summary:
238
+ self.conversation.add_notice(
239
+ f"⚠ startup {summary}",
240
+ level="warning",
241
+ )
242
+ self.status.set_status("UI Ready")
243
+
244
+ def _present_mcp_status(self, event: RuntimeEvent) -> None:
245
+ if event.mcp_status is None:
246
+ return
247
+ status = event.mcp_status
248
+ if status.status == "connected":
249
+ self.conversation.add_notice(
250
+ f"· MCP {status.alias} connected ({status.tool_count} tools)"
251
+ )
252
+ return
253
+ detail = summarize_event_content(
254
+ status.error_summary or status.error_type or "unavailable"
255
+ )
256
+ self.conversation.add_notice(
257
+ f"⚠ MCP {status.alias} unavailable: {detail}",
258
+ level="warning",
259
+ )
260
+
261
+
262
+ def _subagent_summary(value: str) -> str:
263
+ normalized = " ".join(value.split())
264
+ if len(normalized) <= SUBAGENT_TUI_SUMMARY_CHARS:
265
+ return normalized
266
+ return normalized[: SUBAGENT_TUI_SUMMARY_CHARS - 3].rstrip() + "..."