EvoScientist 0.1.0rc1__py3-none-any.whl → 0.1.0rc2__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 (108) hide show
  1. EvoScientist/EvoScientist.py +1 -1
  2. EvoScientist/cli.py +450 -178
  3. EvoScientist/middleware.py +5 -1
  4. EvoScientist/skills/accelerate/SKILL.md +332 -0
  5. EvoScientist/skills/accelerate/references/custom-plugins.md +453 -0
  6. EvoScientist/skills/accelerate/references/megatron-integration.md +489 -0
  7. EvoScientist/skills/accelerate/references/performance.md +525 -0
  8. EvoScientist/skills/bitsandbytes/SKILL.md +411 -0
  9. EvoScientist/skills/bitsandbytes/references/memory-optimization.md +521 -0
  10. EvoScientist/skills/bitsandbytes/references/qlora-training.md +521 -0
  11. EvoScientist/skills/bitsandbytes/references/quantization-formats.md +447 -0
  12. EvoScientist/skills/clip/SKILL.md +253 -0
  13. EvoScientist/skills/clip/references/applications.md +207 -0
  14. EvoScientist/skills/find-skills/SKILL.md +133 -0
  15. EvoScientist/skills/find-skills/scripts/install_skill.py +211 -0
  16. EvoScientist/skills/flash-attention/SKILL.md +367 -0
  17. EvoScientist/skills/flash-attention/references/benchmarks.md +215 -0
  18. EvoScientist/skills/flash-attention/references/transformers-integration.md +293 -0
  19. EvoScientist/skills/langgraph-docs/SKILL.md +36 -0
  20. EvoScientist/skills/llama-cpp/SKILL.md +258 -0
  21. EvoScientist/skills/llama-cpp/references/optimization.md +89 -0
  22. EvoScientist/skills/llama-cpp/references/quantization.md +213 -0
  23. EvoScientist/skills/llama-cpp/references/server.md +125 -0
  24. EvoScientist/skills/lm-evaluation-harness/SKILL.md +490 -0
  25. EvoScientist/skills/lm-evaluation-harness/references/api-evaluation.md +490 -0
  26. EvoScientist/skills/lm-evaluation-harness/references/benchmark-guide.md +488 -0
  27. EvoScientist/skills/lm-evaluation-harness/references/custom-tasks.md +602 -0
  28. EvoScientist/skills/lm-evaluation-harness/references/distributed-eval.md +519 -0
  29. EvoScientist/skills/ml-paper-writing/SKILL.md +937 -0
  30. EvoScientist/skills/ml-paper-writing/references/checklists.md +361 -0
  31. EvoScientist/skills/ml-paper-writing/references/citation-workflow.md +562 -0
  32. EvoScientist/skills/ml-paper-writing/references/reviewer-guidelines.md +367 -0
  33. EvoScientist/skills/ml-paper-writing/references/sources.md +159 -0
  34. EvoScientist/skills/ml-paper-writing/references/writing-guide.md +476 -0
  35. EvoScientist/skills/ml-paper-writing/templates/README.md +251 -0
  36. EvoScientist/skills/ml-paper-writing/templates/aaai2026/README.md +534 -0
  37. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  38. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  39. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bib +111 -0
  40. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bst +1493 -0
  41. EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.sty +315 -0
  42. EvoScientist/skills/ml-paper-writing/templates/acl/README.md +50 -0
  43. EvoScientist/skills/ml-paper-writing/templates/acl/acl.sty +312 -0
  44. EvoScientist/skills/ml-paper-writing/templates/acl/acl_latex.tex +377 -0
  45. EvoScientist/skills/ml-paper-writing/templates/acl/acl_lualatex.tex +101 -0
  46. EvoScientist/skills/ml-paper-writing/templates/acl/acl_natbib.bst +1940 -0
  47. EvoScientist/skills/ml-paper-writing/templates/acl/anthology.bib.txt +26 -0
  48. EvoScientist/skills/ml-paper-writing/templates/acl/custom.bib +70 -0
  49. EvoScientist/skills/ml-paper-writing/templates/acl/formatting.md +326 -0
  50. EvoScientist/skills/ml-paper-writing/templates/colm2025/README.md +3 -0
  51. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bib +11 -0
  52. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bst +1440 -0
  53. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
  54. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.sty +218 -0
  55. EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.tex +305 -0
  56. EvoScientist/skills/ml-paper-writing/templates/colm2025/fancyhdr.sty +485 -0
  57. EvoScientist/skills/ml-paper-writing/templates/colm2025/math_commands.tex +508 -0
  58. EvoScientist/skills/ml-paper-writing/templates/colm2025/natbib.sty +1246 -0
  59. EvoScientist/skills/ml-paper-writing/templates/iclr2026/fancyhdr.sty +485 -0
  60. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bib +24 -0
  61. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bst +1440 -0
  62. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
  63. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.sty +246 -0
  64. EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.tex +414 -0
  65. EvoScientist/skills/ml-paper-writing/templates/iclr2026/math_commands.tex +508 -0
  66. EvoScientist/skills/ml-paper-writing/templates/iclr2026/natbib.sty +1246 -0
  67. EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithm.sty +79 -0
  68. EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithmic.sty +201 -0
  69. EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.bib +75 -0
  70. EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.pdf +0 -0
  71. EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.tex +662 -0
  72. EvoScientist/skills/ml-paper-writing/templates/icml2026/fancyhdr.sty +864 -0
  73. EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.bst +1443 -0
  74. EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.sty +767 -0
  75. EvoScientist/skills/ml-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
  76. EvoScientist/skills/ml-paper-writing/templates/neurips2025/Makefile +36 -0
  77. EvoScientist/skills/ml-paper-writing/templates/neurips2025/extra_pkgs.tex +53 -0
  78. EvoScientist/skills/ml-paper-writing/templates/neurips2025/main.tex +38 -0
  79. EvoScientist/skills/ml-paper-writing/templates/neurips2025/neurips.sty +382 -0
  80. EvoScientist/skills/peft/SKILL.md +431 -0
  81. EvoScientist/skills/peft/references/advanced-usage.md +514 -0
  82. EvoScientist/skills/peft/references/troubleshooting.md +480 -0
  83. EvoScientist/skills/ray-data/SKILL.md +326 -0
  84. EvoScientist/skills/ray-data/references/integration.md +82 -0
  85. EvoScientist/skills/ray-data/references/transformations.md +83 -0
  86. EvoScientist/skills/skill-creator/LICENSE.txt +202 -0
  87. EvoScientist/skills/skill-creator/SKILL.md +356 -0
  88. EvoScientist/skills/skill-creator/references/output-patterns.md +82 -0
  89. EvoScientist/skills/skill-creator/references/workflows.md +28 -0
  90. EvoScientist/skills/skill-creator/scripts/init_skill.py +303 -0
  91. EvoScientist/skills/skill-creator/scripts/package_skill.py +110 -0
  92. EvoScientist/skills/skill-creator/scripts/quick_validate.py +95 -0
  93. EvoScientist/skills/tensorboard/SKILL.md +629 -0
  94. EvoScientist/skills/tensorboard/references/integrations.md +638 -0
  95. EvoScientist/skills/tensorboard/references/profiling.md +545 -0
  96. EvoScientist/skills/tensorboard/references/visualization.md +620 -0
  97. EvoScientist/skills/vllm/SKILL.md +364 -0
  98. EvoScientist/skills/vllm/references/optimization.md +226 -0
  99. EvoScientist/skills/vllm/references/quantization.md +284 -0
  100. EvoScientist/skills/vllm/references/server-deployment.md +255 -0
  101. EvoScientist/skills/vllm/references/troubleshooting.md +447 -0
  102. {evoscientist-0.1.0rc1.dist-info → evoscientist-0.1.0rc2.dist-info}/METADATA +26 -3
  103. evoscientist-0.1.0rc2.dist-info/RECORD +119 -0
  104. evoscientist-0.1.0rc1.dist-info/RECORD +0 -21
  105. {evoscientist-0.1.0rc1.dist-info → evoscientist-0.1.0rc2.dist-info}/WHEEL +0 -0
  106. {evoscientist-0.1.0rc1.dist-info → evoscientist-0.1.0rc2.dist-info}/entry_points.txt +0 -0
  107. {evoscientist-0.1.0rc1.dist-info → evoscientist-0.1.0rc2.dist-info}/licenses/LICENSE +0 -0
  108. {evoscientist-0.1.0rc1.dist-info → evoscientist-0.1.0rc2.dist-info}/top_level.txt +0 -0
EvoScientist/cli.py CHANGED
@@ -74,41 +74,166 @@ async def stream_agent_events(agent: Any, message: str, thread_id: str) -> Async
74
74
  """
75
75
  config = {"configurable": {"thread_id": thread_id}}
76
76
  emitter = StreamEventEmitter()
77
- tracker = ToolCallTracker()
77
+ main_tracker = ToolCallTracker()
78
78
  full_response = ""
79
79
 
80
- # Track sub-agent names by root namespace element
81
- _subagent_names: dict[str, str] = {} # root_ns_element → display name
82
- # Track which task tool_call_ids have been announced
83
- _announced_tasks: set[str] = set()
80
+ # Track sub-agent names
81
+ _key_to_name: dict[str, str] = {} # subagent_key → display name (cache)
82
+ _announced_names: list[str] = [] # ordered queue of announced task names
83
+ _assigned_names: set[str] = set() # names already assigned to a namespace
84
+ _announced_task_ids: list[str] = [] # ordered task tool_call_ids
85
+ _task_id_to_name: dict[str, str] = {} # tool_call_id → sub-agent name
86
+ _subagent_trackers: dict[str, ToolCallTracker] = {} # namespace_key → tracker
87
+
88
+ def _register_task_tool_call(tc_data: dict) -> str | None:
89
+ """Register or update a task tool call, return subagent name if started/updated."""
90
+ tool_id = tc_data.get("id", "")
91
+ if not tool_id:
92
+ return None
93
+ args = tc_data.get("args", {}) or {}
94
+ desc = str(args.get("description", "")).strip()
95
+ sa_name = str(args.get("subagent_type", "")).strip()
96
+ if not sa_name:
97
+ # Fallback to description snippet (may be empty during streaming)
98
+ sa_name = desc[:30] + "..." if len(desc) > 30 else desc
99
+ if not sa_name:
100
+ sa_name = "sub-agent"
101
+
102
+ if tool_id not in _announced_task_ids:
103
+ _announced_task_ids.append(tool_id)
104
+ _announced_names.append(sa_name)
105
+ _task_id_to_name[tool_id] = sa_name
106
+ return sa_name
107
+
108
+ # Update mapping if we learned a better name later
109
+ current = _task_id_to_name.get(tool_id, "sub-agent")
110
+ if sa_name != "sub-agent" and current != sa_name:
111
+ _task_id_to_name[tool_id] = sa_name
112
+ try:
113
+ idx = _announced_task_ids.index(tool_id)
114
+ if idx < len(_announced_names):
115
+ _announced_names[idx] = sa_name
116
+ except ValueError:
117
+ pass
118
+ return sa_name
119
+ return None
84
120
 
85
- def _get_subagent_name(namespace: tuple) -> str | None:
86
- """Get sub-agent name from namespace, or None if main agent.
121
+ def _extract_task_id(namespace: tuple) -> tuple[str | None, str | None]:
122
+ """Extract task tool_call_id from namespace if present.
87
123
 
88
- Any non-empty namespace is a sub-agent. Name is resolved by checking
89
- all registered names for a prefix match against namespace elements.
124
+ Returns (task_id, task_ns_element) or (None, None).
90
125
  """
126
+ for part in namespace:
127
+ part_str = str(part)
128
+ if "task:" in part_str:
129
+ tail = part_str.split("task:", 1)[1]
130
+ task_id = tail.split(":", 1)[0] if tail else ""
131
+ if task_id:
132
+ return task_id, part_str
133
+ return None, None
134
+
135
+ def _next_announced_name() -> str | None:
136
+ """Get next announced name that hasn't been assigned yet."""
137
+ for announced in _announced_names:
138
+ if announced not in _assigned_names:
139
+ _assigned_names.add(announced)
140
+ return announced
141
+ return None
142
+
143
+ def _find_task_id_from_metadata(metadata: dict | None) -> str | None:
144
+ """Try to find a task tool_call_id in metadata."""
145
+ if not metadata:
146
+ return None
147
+ candidates = (
148
+ "tool_call_id",
149
+ "task_id",
150
+ "parent_run_id",
151
+ "root_run_id",
152
+ "run_id",
153
+ )
154
+ for key in candidates:
155
+ val = metadata.get(key)
156
+ if val and val in _task_id_to_name:
157
+ return val
158
+ return None
159
+
160
+ def _get_subagent_key(namespace: tuple, metadata: dict | None) -> str | None:
161
+ """Stable key for tracker/mapping per sub-agent namespace."""
91
162
  if not namespace:
92
163
  return None
93
- root = str(namespace[0]) if namespace else ""
94
- # Exact match
95
- if root in _subagent_names:
96
- return _subagent_names[root]
97
- # Prefix match: namespace root might be "task:abc123" and we
98
- # registered "task:call_xyz" — check if any registered key
99
- # appears as a substring of the root or vice versa
100
- for key, name in _subagent_names.items():
101
- if key in root or root in key:
102
- _subagent_names[root] = name # cache for next lookup
164
+ task_id, task_ns = _extract_task_id(namespace)
165
+ if task_ns:
166
+ return task_ns
167
+ meta_task_id = _find_task_id_from_metadata(metadata)
168
+ if meta_task_id:
169
+ return f"task:{meta_task_id}"
170
+ if metadata:
171
+ for key in ("parent_run_id", "root_run_id", "run_id", "graph_id", "node_id"):
172
+ val = metadata.get(key)
173
+ if val:
174
+ return f"{key}:{val}"
175
+ return str(namespace)
176
+
177
+ def _get_subagent_name(namespace: tuple, metadata: dict | None) -> str | None:
178
+ """Resolve sub-agent name from namespace, or None if main agent.
179
+
180
+ Priority:
181
+ 0) metadata["lc_agent_name"] — most reliable, set by DeepAgents framework.
182
+ 1) Match task_id embedded in namespace to announced tool_call_id.
183
+ 2) Use cached key mapping (only real names, never "sub-agent").
184
+ 3) Queue-based: assign next announced name to this key.
185
+ 4) Fallback: return "sub-agent" WITHOUT caching.
186
+ """
187
+ if not namespace:
188
+ return None
189
+
190
+ key = _get_subagent_key(namespace, metadata) or str(namespace)
191
+
192
+ # 0) lc_agent_name from metadata — the REAL sub-agent name
193
+ # set by the DeepAgents framework on every namespace event.
194
+ if metadata:
195
+ lc_name = metadata.get("lc_agent_name", "")
196
+ if isinstance(lc_name, str):
197
+ lc_name = lc_name.strip()
198
+ # Filter out generic/framework names
199
+ if lc_name and lc_name not in (
200
+ "sub-agent", "agent", "tools", "EvoScientist",
201
+ "LangGraph", "",
202
+ ):
203
+ _key_to_name[key] = lc_name
204
+ return lc_name
205
+
206
+ # 1) Resolve by task_id if present in namespace
207
+ task_id, _task_ns = _extract_task_id(namespace)
208
+ if task_id and task_id in _task_id_to_name:
209
+ name = _task_id_to_name[task_id]
210
+ if name and name != "sub-agent":
211
+ _assigned_names.add(name)
212
+ _key_to_name[key] = name
103
213
  return name
104
- # Auto-register: infer from namespace string
105
- if ":" in root:
106
- inferred = root.split(":")[0]
107
- else:
108
- inferred = root
109
- name = inferred or "sub-agent"
110
- _subagent_names[root] = name
111
- return name
214
+
215
+ meta_task_id = _find_task_id_from_metadata(metadata)
216
+ if meta_task_id and meta_task_id in _task_id_to_name:
217
+ name = _task_id_to_name[meta_task_id]
218
+ if name and name != "sub-agent":
219
+ _assigned_names.add(name)
220
+ _key_to_name[key] = name
221
+ return name
222
+
223
+ # 2) Cached real name for this key (skip if it's "sub-agent")
224
+ cached = _key_to_name.get(key)
225
+ if cached and cached != "sub-agent":
226
+ return cached
227
+
228
+ # 3) Assign next announced name from queue (skip "sub-agent" entries)
229
+ for announced in _announced_names:
230
+ if announced not in _assigned_names and announced != "sub-agent":
231
+ _assigned_names.add(announced)
232
+ _key_to_name[key] = announced
233
+ return announced
234
+
235
+ # 4) No real names available yet — return generic WITHOUT caching
236
+ return "sub-agent"
112
237
 
113
238
  try:
114
239
  async for chunk in agent.astream(
@@ -131,20 +256,26 @@ async def stream_agent_events(agent: Any, message: str, thread_id: str) -> Async
131
256
  # (message, metadata) — no namespace
132
257
  data = chunk
133
258
 
134
- # Unpack message from data
259
+ # Unpack message + metadata from data
135
260
  msg: Any
261
+ metadata: dict = {}
136
262
  if isinstance(data, tuple) and len(data) >= 2:
137
263
  msg = data[0]
264
+ metadata = data[1] or {}
138
265
  else:
139
266
  msg = data
140
267
 
141
- subagent = _get_subagent_name(namespace)
268
+ subagent = _get_subagent_name(namespace, metadata)
269
+ subagent_tracker = None
270
+ if subagent:
271
+ tracker_key = _get_subagent_key(namespace, metadata) or str(namespace)
272
+ subagent_tracker = _subagent_trackers.setdefault(tracker_key, ToolCallTracker())
142
273
 
143
274
  # Process AIMessageChunk / AIMessage
144
275
  if isinstance(msg, (AIMessageChunk, AIMessage)):
145
276
  if subagent:
146
277
  # Sub-agent content — emit sub-agent events
147
- for ev in _process_chunk_content(msg, emitter, tracker):
278
+ for ev in _process_chunk_content(msg, emitter, subagent_tracker):
148
279
  if ev.type == "tool_call":
149
280
  yield emitter.subagent_tool_call(
150
281
  subagent, ev.data["name"], ev.data["args"], ev.data.get("id", "")
@@ -164,50 +295,54 @@ async def stream_agent_events(agent: Any, message: str, thread_id: str) -> Async
164
295
  ).data
165
296
  else:
166
297
  # Main agent content
167
- for ev in _process_chunk_content(msg, emitter, tracker):
298
+ for ev in _process_chunk_content(msg, emitter, main_tracker):
168
299
  if ev.type == "text":
169
300
  full_response += ev.data.get("content", "")
170
301
  yield ev.data
171
302
 
172
303
  if hasattr(msg, "tool_calls") and msg.tool_calls:
173
- for ev in _process_tool_calls(msg.tool_calls, emitter, tracker):
304
+ for ev in _process_tool_calls(msg.tool_calls, emitter, main_tracker):
174
305
  yield ev.data
175
306
  # Detect task tool calls → announce sub-agent
176
307
  tc_data = ev.data
177
308
  if tc_data.get("name") == "task":
178
- tool_id = tc_data.get("id", "")
179
- if tool_id and tool_id not in _announced_tasks:
180
- _announced_tasks.add(tool_id)
181
- args = tc_data.get("args", {})
182
- sa_name = args.get("subagent_type", "").strip()
183
- desc = args.get("description", "").strip()
184
- # Use subagent_type as name; fall back to description snippet
185
- if not sa_name:
186
- sa_name = desc[:30] + "..." if len(desc) > 30 else desc
187
- if not sa_name:
188
- sa_name = "sub-agent"
189
- # Pre-register name so namespace lookup finds it
190
- _subagent_names[f"task:{tool_id}"] = sa_name
191
- yield emitter.subagent_start(sa_name, desc).data
309
+ started_name = _register_task_tool_call(tc_data)
310
+ if started_name:
311
+ desc = str(tc_data.get("args", {}).get("description", "")).strip()
312
+ yield emitter.subagent_start(started_name, desc).data
192
313
 
193
314
  # Process ToolMessage (tool execution result)
194
315
  elif hasattr(msg, "type") and msg.type == "tool":
195
316
  if subagent:
317
+ if subagent_tracker:
318
+ subagent_tracker.finalize_all()
319
+ for info in subagent_tracker.emit_all_pending():
320
+ yield emitter.subagent_tool_call(
321
+ subagent,
322
+ info.name,
323
+ info.args,
324
+ info.id,
325
+ ).data
196
326
  name = getattr(msg, "name", "unknown")
197
327
  raw_content = str(getattr(msg, "content", ""))
198
328
  content = raw_content[:DisplayLimits.TOOL_RESULT_MAX]
199
329
  success = is_success(content)
200
330
  yield emitter.subagent_tool_result(subagent, name, content, success).data
201
331
  else:
202
- for ev in _process_tool_result(msg, emitter, tracker):
332
+ for ev in _process_tool_result(msg, emitter, main_tracker):
203
333
  yield ev.data
334
+ # Tool result can re-emit tool_call with full args; update task mapping
335
+ if ev.type == "tool_call" and ev.data.get("name") == "task":
336
+ started_name = _register_task_tool_call(ev.data)
337
+ if started_name:
338
+ desc = str(ev.data.get("args", {}).get("description", "")).strip()
339
+ yield emitter.subagent_start(started_name, desc).data
204
340
  # Check if this is a task result → sub-agent ended
205
341
  name = getattr(msg, "name", "")
206
342
  if name == "task":
207
343
  tool_call_id = getattr(msg, "tool_call_id", "")
208
- # Find the sub-agent name for this task
209
- sa_key = f"task:{tool_call_id}"
210
- sa_name = _subagent_names.get(sa_key, "sub-agent")
344
+ # Find the sub-agent name via tool_call_id map
345
+ sa_name = _task_id_to_name.get(tool_call_id, "sub-agent")
211
346
  yield emitter.subagent_end(sa_name).data
212
347
 
213
348
  except Exception as e:
@@ -403,12 +538,14 @@ class StreamState:
403
538
  # Sub-agent tracking
404
539
  self.subagents: list[SubAgentState] = []
405
540
  self._subagent_map: dict[str, SubAgentState] = {} # name → state
541
+ # Todo list tracking
542
+ self.todo_items: list[dict] = []
543
+ # Latest text segment (reset on each tool_call)
544
+ self.latest_text = ""
406
545
 
407
546
  def _get_or_create_subagent(self, name: str, description: str = "") -> SubAgentState:
408
547
  if name not in self._subagent_map:
409
- # Check if there's a generic "sub-agent" entry that should be merged
410
- # This happens when namespace events arrive before the task tool call
411
- # registers the proper name
548
+ # Case 1: real name arrives, "sub-agent" entry exists rename it
412
549
  if name != "sub-agent" and "sub-agent" in self._subagent_map:
413
550
  old_sa = self._subagent_map.pop("sub-agent")
414
551
  old_sa.name = name
@@ -416,13 +553,41 @@ class StreamState:
416
553
  old_sa.description = description
417
554
  self._subagent_map[name] = old_sa
418
555
  return old_sa
556
+ # Case 2: "sub-agent" arrives but a pre-registered real-name entry
557
+ # exists with no tool calls → merge into it
558
+ if name == "sub-agent":
559
+ active_named = [
560
+ sa for sa in self.subagents
561
+ if sa.is_active and sa.name != "sub-agent"
562
+ ]
563
+ if len(active_named) == 1 and not active_named[0].tool_calls:
564
+ self._subagent_map[name] = active_named[0]
565
+ return active_named[0]
419
566
  sa = SubAgentState(name, description)
420
567
  self.subagents.append(sa)
421
568
  self._subagent_map[name] = sa
422
- elif description and not self._subagent_map[name].description:
423
- self._subagent_map[name].description = description
569
+ else:
570
+ existing = self._subagent_map[name]
571
+ if description and not existing.description:
572
+ existing.description = description
573
+ # If this entry was created as "sub-agent" placeholder and the
574
+ # actual name is different, update.
575
+ if name != "sub-agent" and existing.name == "sub-agent":
576
+ existing.name = name
424
577
  return self._subagent_map[name]
425
578
 
579
+ def _resolve_subagent_name(self, name: str) -> str:
580
+ """Resolve "sub-agent" to the single active named sub-agent when possible."""
581
+ if name != "sub-agent":
582
+ return name
583
+ active_named = [
584
+ sa.name for sa in self.subagents
585
+ if sa.is_active and sa.name != "sub-agent"
586
+ ]
587
+ if len(active_named) == 1:
588
+ return active_named[0]
589
+ return name
590
+
426
591
  def handle_event(self, event: dict) -> str:
427
592
  """Process a single stream event, update internal state, return event type."""
428
593
  event_type: str = event.get("type", "")
@@ -437,18 +602,23 @@ class StreamState:
437
602
  self.is_thinking = False
438
603
  self.is_responding = True
439
604
  self.is_processing = False
440
- self.response_text += event.get("content", "")
605
+ text_content = event.get("content", "")
606
+ self.response_text += text_content
607
+ self.latest_text += text_content
441
608
 
442
609
  elif event_type == "tool_call":
443
610
  self.is_thinking = False
444
611
  self.is_responding = False
445
612
  self.is_processing = False
613
+ self.latest_text = "" # Reset — next text segment is a new message
446
614
 
447
615
  tool_id = event.get("id", "")
616
+ tool_name = event.get("name", "unknown")
617
+ tool_args = event.get("args", {})
448
618
  tc_data = {
449
619
  "id": tool_id,
450
- "name": event.get("name", "unknown"),
451
- "args": event.get("args", {}),
620
+ "name": tool_name,
621
+ "args": tool_args,
452
622
  }
453
623
 
454
624
  if tool_id:
@@ -463,12 +633,25 @@ class StreamState:
463
633
  else:
464
634
  self.tool_calls.append(tc_data)
465
635
 
636
+ # Capture todo items from write_todos args (most reliable source)
637
+ if tool_name == "write_todos":
638
+ todos = tool_args.get("todos", [])
639
+ if isinstance(todos, list) and todos:
640
+ self.todo_items = todos
641
+
466
642
  elif event_type == "tool_result":
467
643
  self.is_processing = True
644
+ result_name = event.get("name", "unknown")
645
+ result_content = event.get("content", "")
468
646
  self.tool_results.append({
469
- "name": event.get("name", "unknown"),
470
- "content": event.get("content", ""),
647
+ "name": result_name,
648
+ "content": result_content,
471
649
  })
650
+ # Update todo list from write_todos / read_todos results (fallback)
651
+ if result_name in ("write_todos", "read_todos"):
652
+ parsed = _parse_todo_items(result_content)
653
+ if parsed:
654
+ self.todo_items = parsed
472
655
 
473
656
  elif event_type == "subagent_start":
474
657
  name = event.get("name", "sub-agent")
@@ -477,7 +660,7 @@ class StreamState:
477
660
  sa.is_active = True
478
661
 
479
662
  elif event_type == "subagent_tool_call":
480
- sa_name = event.get("subagent", "sub-agent")
663
+ sa_name = self._resolve_subagent_name(event.get("subagent", "sub-agent"))
481
664
  sa = self._get_or_create_subagent(sa_name)
482
665
  sa.add_tool_call(
483
666
  event.get("name", "unknown"),
@@ -486,7 +669,7 @@ class StreamState:
486
669
  )
487
670
 
488
671
  elif event_type == "subagent_tool_result":
489
- sa_name = event.get("subagent", "sub-agent")
672
+ sa_name = self._resolve_subagent_name(event.get("subagent", "sub-agent"))
490
673
  sa = self._get_or_create_subagent(sa_name)
491
674
  sa.add_tool_result(
492
675
  event.get("name", "unknown"),
@@ -495,9 +678,15 @@ class StreamState:
495
678
  )
496
679
 
497
680
  elif event_type == "subagent_end":
498
- name = event.get("name", "sub-agent")
681
+ name = self._resolve_subagent_name(event.get("name", "sub-agent"))
499
682
  if name in self._subagent_map:
500
683
  self._subagent_map[name].is_active = False
684
+ elif name == "sub-agent":
685
+ # Couldn't resolve — deactivate the oldest active sub-agent
686
+ for sa in self.subagents:
687
+ if sa.is_active:
688
+ sa.is_active = False
689
+ break
501
690
 
502
691
  elif event_type == "done":
503
692
  self.is_processing = False
@@ -518,12 +707,14 @@ class StreamState:
518
707
  return {
519
708
  "thinking_text": self.thinking_text,
520
709
  "response_text": self.response_text,
710
+ "latest_text": self.latest_text,
521
711
  "tool_calls": self.tool_calls,
522
712
  "tool_results": self.tool_results,
523
713
  "is_thinking": self.is_thinking,
524
714
  "is_responding": self.is_responding,
525
715
  "is_processing": self.is_processing,
526
716
  "subagents": self.subagents,
717
+ "todo_items": self.todo_items,
527
718
  }
528
719
 
529
720
 
@@ -536,43 +727,58 @@ def _parse_todo_items(content: str) -> list[dict] | None:
536
727
 
537
728
  Attempts to extract a list of dicts with 'status' and 'content' keys
538
729
  from the tool result string. Returns None if parsing fails.
730
+
731
+ Handles formats like:
732
+ - Raw JSON/Python list: [{"content": "...", "status": "..."}]
733
+ - Prefixed: "Updated todo list to [{'content': '...', ...}]"
539
734
  """
540
735
  import ast
541
736
  import json
542
737
 
543
738
  content = content.strip()
544
739
 
545
- # Try JSON first
546
- try:
547
- data = json.loads(content)
548
- if isinstance(data, list) and data and isinstance(data[0], dict):
549
- return data
550
- except (json.JSONDecodeError, ValueError):
551
- pass
552
-
553
- # Try Python literal
554
- try:
555
- data = ast.literal_eval(content)
556
- if isinstance(data, list) and data and isinstance(data[0], dict):
557
- return data
558
- except (ValueError, SyntaxError):
559
- pass
740
+ def _try_parse(text: str) -> list[dict] | None:
741
+ """Try JSON then Python literal parsing."""
742
+ text = text.strip()
743
+ try:
744
+ data = json.loads(text)
745
+ if isinstance(data, list) and data and isinstance(data[0], dict):
746
+ return data
747
+ except (json.JSONDecodeError, ValueError):
748
+ pass
749
+ try:
750
+ data = ast.literal_eval(text)
751
+ if isinstance(data, list) and data and isinstance(data[0], dict):
752
+ return data
753
+ except (ValueError, SyntaxError):
754
+ pass
755
+ return None
560
756
 
561
- # Try to find a list embedded in the output
757
+ # Try the full content directly
758
+ result = _try_parse(content)
759
+ if result:
760
+ return result
761
+
762
+ # Extract embedded [...] from content (e.g. "Updated todo list to [{...}]")
763
+ bracket_start = content.find("[")
764
+ if bracket_start != -1:
765
+ bracket_end = content.rfind("]")
766
+ if bracket_end > bracket_start:
767
+ embedded = content[bracket_start:bracket_end + 1]
768
+ result = _try_parse(embedded)
769
+ if result:
770
+ return result
771
+
772
+ # Try line-by-line scan
562
773
  for line in content.split("\n"):
563
774
  line = line.strip()
564
- if line.startswith("[") and line.endswith("]"):
565
- try:
566
- data = json.loads(line)
567
- if isinstance(data, list):
568
- return data
569
- except (json.JSONDecodeError, ValueError):
570
- try:
571
- data = ast.literal_eval(line)
572
- if isinstance(data, list):
573
- return data
574
- except (ValueError, SyntaxError):
575
- pass
775
+ if "[" in line:
776
+ start = line.find("[")
777
+ end = line.rfind("]")
778
+ if end > start:
779
+ result = _try_parse(line[start:end + 1])
780
+ if result:
781
+ return result
576
782
 
577
783
  return None
578
784
 
@@ -701,14 +907,14 @@ def _render_tool_call_line(tc: dict, tr: dict | None) -> Text:
701
907
 
702
908
 
703
909
  def _render_subagent_section(sa: 'SubAgentState', compact: bool = False) -> list:
704
- """Render a sub-agent's activity as a compact indented section.
910
+ """Render a sub-agent's activity as a bordered section.
705
911
 
706
912
  Args:
707
913
  sa: Sub-agent state to render
708
- compact: If True, render minimal 1-2 line summary (for final display)
914
+ compact: If True, render minimal 1-line summary (completed sub-agents)
709
915
 
710
- Completed tools are collapsed into a summary line.
711
- Only the currently running tool is shown expanded.
916
+ Header uses "Cooking with {name}" style matching task tool format.
917
+ Active sub-agents show bordered tool list; completed ones collapse to 1 line.
712
918
  """
713
919
  elements = []
714
920
  BORDER = "dim cyan" if sa.is_active else "dim"
@@ -729,59 +935,49 @@ def _render_subagent_section(sa: 'SubAgentState', compact: bool = False) -> list
729
935
  succeeded = sum(1 for _, tr in completed if tr.get("success", True))
730
936
  failed = len(completed) - succeeded
731
937
 
732
- # --- Compact mode: 1-2 line summary for final display ---
938
+ # Build display name
939
+ display_name = f"Cooking with {sa.name}"
940
+ if sa.description:
941
+ desc = sa.description[:50] + "..." if len(sa.description) > 50 else sa.description
942
+ display_name += f" \u2014 {desc}"
943
+
944
+ # --- Compact mode: 1-line summary for completed sub-agents ---
733
945
  if compact:
734
946
  line = Text()
735
947
  if not sa.is_active:
736
- line.append(" \u2713 ", style="green")
737
- line.append(sa.name, style="bold green")
948
+ line.append("\u2713 ", style="green")
949
+ line.append(display_name, style="green dim")
950
+ total = len(valid_calls)
951
+ line.append(f" ({total} tools)", style="dim")
738
952
  else:
739
- line.append(" \u25b6 ", style="cyan")
740
- line.append(sa.name, style="bold cyan")
741
- if sa.description:
742
- desc = sa.description[:50] + "..." if len(sa.description) > 50 else sa.description
743
- line.append(f" \u2014 {desc}", style="dim")
953
+ line.append("\u25b6 ", style="cyan")
954
+ line.append(display_name, style="bold cyan")
744
955
  elements.append(line)
745
- # Stats line
746
- if valid_calls:
747
- stats = Text(" ")
748
- stats.append(f"{succeeded} completed", style="dim green")
749
- if failed > 0:
750
- stats.append(f" \u00b7 {failed} failed", style="dim red")
751
- if pending:
752
- stats.append(f" \u00b7 {len(pending)} running", style="dim yellow")
753
- elements.append(stats)
754
956
  return elements
755
957
 
756
958
  # --- Full mode: bordered section for Live streaming ---
757
- # Shows every tool call individually with status indicators
758
959
 
759
960
  # Header
760
961
  header = Text()
761
- header.append(" \u250c ", style=BORDER)
962
+ header.append("\u250c ", style=BORDER)
762
963
  if sa.is_active:
763
- header.append(sa.name, style="bold cyan")
964
+ header.append(f"\u25b6 {display_name}", style="bold cyan")
764
965
  else:
765
- header.append(sa.name, style="bold green")
766
- header.append(" \u2713", style="green")
767
- if sa.description:
768
- desc = sa.description[:55] + "..." if len(sa.description) > 55 else sa.description
769
- header.append(f" \u2014 {desc}", style="dim")
966
+ header.append(f"\u2713 {display_name}", style="bold green")
770
967
  elements.append(header)
771
968
 
772
969
  # Show every tool call with its status
773
970
  for tc, tr in completed:
774
- tc_line = Text(" \u2502 ", style=BORDER)
971
+ tc_line = Text("\u2502 ", style=BORDER)
775
972
  tc_name = format_tool_compact(tc["name"], tc.get("args"))
776
973
  if tr.get("success", True):
777
974
  tc_line.append(f"\u2713 {tc_name}", style="green")
778
975
  else:
779
976
  tc_line.append(f"\u2717 {tc_name}", style="red")
780
- # Show first line of error
781
977
  content = tr.get("content", "")
782
978
  first_line = content.strip().split("\n")[0][:70]
783
979
  if first_line:
784
- err_line = Text(" \u2502 ", style=BORDER)
980
+ err_line = Text("\u2502 ", style=BORDER)
785
981
  err_line.append(f"\u2514 {first_line}", style="red dim")
786
982
  elements.append(tc_line)
787
983
  elements.append(err_line)
@@ -790,29 +986,64 @@ def _render_subagent_section(sa: 'SubAgentState', compact: bool = False) -> list
790
986
 
791
987
  # Pending/running tools
792
988
  for tc in pending:
793
- tc_line = Text(" \u2502 ", style=BORDER)
989
+ tc_line = Text("\u2502 ", style=BORDER)
794
990
  tc_name = format_tool_compact(tc["name"], tc.get("args"))
795
991
  tc_line.append(f"\u25cf {tc_name}", style="bold yellow")
796
992
  elements.append(tc_line)
797
- spinner_line = Text(" \u2502 ", style=BORDER)
993
+ spinner_line = Text("\u2502 ", style=BORDER)
798
994
  spinner_line.append("\u21bb running...", style="yellow dim")
799
995
  elements.append(spinner_line)
800
996
 
801
997
  # Footer
802
998
  if not sa.is_active:
803
999
  total = len(valid_calls)
804
- footer = Text(f" \u2514 done ({total} tools)", style="dim green")
1000
+ footer = Text(f"\u2514 done ({total} tools)", style="dim green")
805
1001
  elements.append(footer)
806
1002
  elif valid_calls:
807
- footer = Text(" \u2514 running...", style="dim cyan")
1003
+ footer = Text("\u2514 running...", style="dim cyan")
808
1004
  elements.append(footer)
809
1005
 
810
1006
  return elements
811
1007
 
812
1008
 
1009
+ def _render_todo_panel(todo_items: list[dict]) -> Panel:
1010
+ """Render a bordered Task List panel from todo items.
1011
+
1012
+ Matches the style: cyan border, status icons per item.
1013
+ """
1014
+ lines = Text()
1015
+ for i, item in enumerate(todo_items):
1016
+ if i > 0:
1017
+ lines.append("\n")
1018
+ status = str(item.get("status", "todo")).lower()
1019
+ content_text = str(item.get("content", item.get("task", item.get("title", ""))))
1020
+
1021
+ if status in ("done", "completed", "complete"):
1022
+ symbol = "\u2713" # ✓
1023
+ style = "green dim"
1024
+ elif status in ("active", "in_progress", "in-progress", "working"):
1025
+ symbol = "\u23f3" # ⏳
1026
+ style = "yellow"
1027
+ else:
1028
+ symbol = "\u25a1" # □
1029
+ style = "dim"
1030
+
1031
+ lines.append(f"{symbol} ", style=style)
1032
+ lines.append(content_text, style=style)
1033
+
1034
+ return Panel(
1035
+ lines,
1036
+ title="Task List",
1037
+ title_align="center",
1038
+ border_style="cyan",
1039
+ padding=(0, 1),
1040
+ )
1041
+
1042
+
813
1043
  def create_streaming_display(
814
1044
  thinking_text: str = "",
815
1045
  response_text: str = "",
1046
+ latest_text: str = "",
816
1047
  tool_calls: list | None = None,
817
1048
  tool_results: list | None = None,
818
1049
  is_thinking: bool = False,
@@ -821,6 +1052,7 @@ def create_streaming_display(
821
1052
  is_processing: bool = False,
822
1053
  show_thinking: bool = True,
823
1054
  subagents: list | None = None,
1055
+ todo_items: list | None = None,
824
1056
  ) -> Any:
825
1057
  """Create Rich display layout for streaming output.
826
1058
 
@@ -855,61 +1087,95 @@ def create_streaming_display(
855
1087
 
856
1088
  # Tool calls and results paired display
857
1089
  # Collapse older completed tools to prevent overflow in Live mode
1090
+ # Task tool calls are ALWAYS visible (they represent sub-agent delegations)
858
1091
  MAX_VISIBLE_TOOLS = 4
1092
+ MAX_VISIBLE_RUNNING = 3
859
1093
 
860
1094
  if tool_calls:
861
- # Split into completed and pending/running
862
- completed_tools = []
863
- recent_tools = [] # last few completed + all pending
1095
+ # Split into categories
1096
+ completed_regular = [] # completed non-task tools
1097
+ task_tools = [] # task tools (always visible)
1098
+ running_regular = [] # running non-task tools
864
1099
 
865
1100
  for i, tc in enumerate(tool_calls):
866
1101
  has_result = i < len(tool_results)
867
1102
  tr = tool_results[i] if has_result else None
868
- if has_result:
869
- completed_tools.append((tc, tr))
1103
+ is_task = tc.get('name') == 'task'
1104
+
1105
+ if is_task:
1106
+ # Skip task calls with empty args (still streaming)
1107
+ if tc.get('args'):
1108
+ task_tools.append((tc, tr))
1109
+ elif has_result:
1110
+ completed_regular.append((tc, tr))
870
1111
  else:
871
- recent_tools.append((tc, None))
872
-
873
- # Determine how many completed tools to show
874
- # Keep the last few completed + all pending within MAX_VISIBLE_TOOLS
875
- slots_for_completed = max(0, MAX_VISIBLE_TOOLS - len(recent_tools))
876
- hidden_completed = completed_tools[:-slots_for_completed] if slots_for_completed and len(completed_tools) > slots_for_completed else (completed_tools if not slots_for_completed else [])
877
- visible_completed = completed_tools[-slots_for_completed:] if slots_for_completed else []
878
-
879
- # Summary line for hidden completed tools
880
- if hidden_completed:
881
- ok = sum(1 for _, tr in hidden_completed if is_success(tr.get('content', '')))
882
- fail = len(hidden_completed) - ok
1112
+ running_regular.append((tc, None))
1113
+
1114
+ # --- Completed regular tools (collapsible) ---
1115
+ slots = max(0, MAX_VISIBLE_TOOLS - len(running_regular))
1116
+ hidden = completed_regular[:-slots] if slots and len(completed_regular) > slots else (completed_regular if not slots else [])
1117
+ visible = completed_regular[-slots:] if slots else []
1118
+
1119
+ if hidden:
1120
+ ok = sum(1 for _, tr in hidden if is_success(tr.get('content', '')))
1121
+ fail = len(hidden) - ok
883
1122
  summary = Text()
884
1123
  summary.append(f"\u2713 {ok} completed", style="dim green")
885
1124
  if fail > 0:
886
1125
  summary.append(f" | {fail} failed", style="dim red")
887
1126
  elements.append(summary)
888
1127
 
889
- # Render visible completed tools (compact: 1 line each, no result expansion)
890
- for tc, tr in visible_completed:
1128
+ for tc, tr in visible:
891
1129
  elements.append(_render_tool_call_line(tc, tr))
892
- # Only expand result for write_todos (useful) or errors
893
1130
  content = tr.get('content', '') if tr else ''
894
- if tc.get('name') == 'write_todos' or (tr and not is_success(content)):
1131
+ if tr and not is_success(content):
895
1132
  result_elements = format_tool_result_compact(
896
- tr['name'],
897
- content,
898
- max_lines=5,
1133
+ tr['name'], content, max_lines=5,
899
1134
  )
900
1135
  elements.extend(result_elements)
901
1136
 
902
- # Render pending/running tools (expanded with spinner)
903
- for tc, tr in recent_tools:
1137
+ # --- Running regular tools (limit visible) ---
1138
+ hidden_running = len(running_regular) - MAX_VISIBLE_RUNNING
1139
+ if hidden_running > 0:
1140
+ summary = Text()
1141
+ summary.append(f"\u25cf {hidden_running} more running...", style="dim yellow")
1142
+ elements.append(summary)
1143
+ running_regular = running_regular[-MAX_VISIBLE_RUNNING:]
1144
+
1145
+ for tc, tr in running_regular:
904
1146
  elements.append(_render_tool_call_line(tc, tr))
905
- if tc.get('name') != 'task':
906
- spinner = Spinner("dots", text=" Running...", style="yellow")
907
- elements.append(spinner)
1147
+ spinner = Spinner("dots", text=" Running...", style="yellow")
1148
+ elements.append(spinner)
1149
+
1150
+ # Task tool calls are rendered as part of sub-agent sections below
1151
+
1152
+ # Response text handling
1153
+ has_pending_tools = len(tool_calls) > len(tool_results)
1154
+ any_active_subagent = any(sa.is_active for sa in subagents)
1155
+ has_used_tools = len(tool_calls) > 0
1156
+ all_done = not has_pending_tools and not any_active_subagent and not is_processing
1157
+
1158
+ # Intermediate narration (tools still running) — dim italic above Task List
1159
+ if latest_text and has_used_tools and not all_done:
1160
+ preview = latest_text.strip()
1161
+ if preview:
1162
+ last_line = preview.split("\n")[-1].strip()
1163
+ if last_line:
1164
+ if len(last_line) > 80:
1165
+ last_line = last_line[:77] + "..."
1166
+ elements.append(Text(f" {last_line}", style="dim italic"))
1167
+
1168
+ # Task List panel (persistent, updates on write_todos / read_todos)
1169
+ todo_items = todo_items or []
1170
+ if todo_items:
1171
+ elements.append(Text("")) # blank separator
1172
+ elements.append(_render_todo_panel(todo_items))
908
1173
 
909
1174
  # Sub-agent activity sections
1175
+ # Active: full bordered view; Completed: compact 1-line summary
910
1176
  for sa in subagents:
911
1177
  if sa.tool_calls or sa.is_active:
912
- elements.extend(_render_subagent_section(sa))
1178
+ elements.extend(_render_subagent_section(sa, compact=not sa.is_active))
913
1179
 
914
1180
  # Processing state after tool execution
915
1181
  if is_processing and not is_thinking and not is_responding and not response_text:
@@ -919,25 +1185,10 @@ def create_streaming_display(
919
1185
  spinner = Spinner("dots", text=" Analyzing results...", style="cyan")
920
1186
  elements.append(spinner)
921
1187
 
922
- # Response text display logic
923
- has_pending_tools = len(tool_calls) > len(tool_results)
924
- any_active_subagent = any(sa.is_active for sa in subagents)
925
- has_used_tools = len(tool_calls) > 0
926
-
927
- if response_text and not has_pending_tools and not any_active_subagent:
928
- if has_used_tools:
929
- # Tools were used — treat all text as intermediate during Live streaming.
930
- # Final rendering is handled by display_final_results().
931
- preview = response_text
932
- if len(preview) > 200:
933
- preview = "..." + preview[-197:]
934
- for line in preview.strip().split("\n")[-3:]:
935
- if line.strip():
936
- elements.append(Text(f" {line.strip()}", style="dim italic"))
937
- else:
938
- # Pure text response (no tools used) — render as Markdown
939
- elements.append(Text("")) # blank separator
940
- elements.append(Markdown(response_text))
1188
+ # Final response render as Markdown when all work is done
1189
+ if response_text and all_done:
1190
+ elements.append(Text("")) # blank separator
1191
+ elements.append(Markdown(response_text))
941
1192
  elif is_responding and not thinking_text and not has_pending_tools:
942
1193
  elements.append(Text("Generating response...", style="dim"))
943
1194
 
@@ -1006,6 +1257,11 @@ def display_final_results(
1006
1257
 
1007
1258
  console.print()
1008
1259
 
1260
+ # Task List panel in final output
1261
+ if state.todo_items:
1262
+ console.print(_render_todo_panel(state.todo_items))
1263
+ console.print()
1264
+
1009
1265
  if state.response_text:
1010
1266
  console.print()
1011
1267
  console.print(Markdown(state.response_text))
@@ -1126,15 +1382,26 @@ def cmd_interactive(agent: Any, show_thinking: bool = True, workspace_dir: str |
1126
1382
  enable_history_search=True,
1127
1383
  )
1128
1384
 
1385
+ def _print_separator():
1386
+ """Print a horizontal separator line spanning the terminal width."""
1387
+ width = console.size.width
1388
+ console.print(Text("\u2500" * width, style="dim"))
1389
+
1390
+ _print_separator()
1129
1391
  while True:
1130
1392
  try:
1131
1393
  user_input = session.prompt(
1132
- HTML('<ansigreen><b>You:</b></ansigreen> ')
1394
+ HTML('<ansiblue><b>&gt;</b></ansiblue> ')
1133
1395
  ).strip()
1134
1396
 
1135
1397
  if not user_input:
1398
+ # Erase the empty prompt line so it looks like nothing happened
1399
+ sys.stdout.write("\033[A\033[2K\r")
1400
+ sys.stdout.flush()
1136
1401
  continue
1137
1402
 
1403
+ _print_separator()
1404
+
1138
1405
  # Special commands
1139
1406
  if user_input.lower() in ("/exit", "/quit", "/q"):
1140
1407
  console.print("[dim]Goodbye![/dim]")
@@ -1160,6 +1427,7 @@ def cmd_interactive(agent: Any, show_thinking: bool = True, workspace_dir: str |
1160
1427
  # Stream agent response
1161
1428
  console.print()
1162
1429
  _run_streaming(agent, user_input, thread_id, show_thinking, interactive=True)
1430
+ _print_separator()
1163
1431
 
1164
1432
  except KeyboardInterrupt:
1165
1433
  console.print("\n[dim]Goodbye![/dim]")
@@ -1180,7 +1448,11 @@ def cmd_run(agent: Any, prompt: str, thread_id: str | None = None, show_thinking
1180
1448
  """
1181
1449
  thread_id = thread_id or str(uuid.uuid4())
1182
1450
 
1183
- console.print(Panel(f"[bold cyan]Query:[/bold cyan]\n{prompt}"))
1451
+ width = console.size.width
1452
+ sep = Text("\u2500" * width, style="dim")
1453
+ console.print(sep)
1454
+ console.print(Text(f"> {prompt}"))
1455
+ console.print(sep)
1184
1456
  console.print(f"[dim]Thread: {thread_id}[/dim]")
1185
1457
  if workspace_dir:
1186
1458
  console.print(f"[dim]Workspace: {workspace_dir}[/dim]")