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.
- EvoScientist/EvoScientist.py +1 -1
- EvoScientist/cli.py +450 -178
- EvoScientist/middleware.py +5 -1
- EvoScientist/skills/accelerate/SKILL.md +332 -0
- EvoScientist/skills/accelerate/references/custom-plugins.md +453 -0
- EvoScientist/skills/accelerate/references/megatron-integration.md +489 -0
- EvoScientist/skills/accelerate/references/performance.md +525 -0
- EvoScientist/skills/bitsandbytes/SKILL.md +411 -0
- EvoScientist/skills/bitsandbytes/references/memory-optimization.md +521 -0
- EvoScientist/skills/bitsandbytes/references/qlora-training.md +521 -0
- EvoScientist/skills/bitsandbytes/references/quantization-formats.md +447 -0
- EvoScientist/skills/clip/SKILL.md +253 -0
- EvoScientist/skills/clip/references/applications.md +207 -0
- EvoScientist/skills/find-skills/SKILL.md +133 -0
- EvoScientist/skills/find-skills/scripts/install_skill.py +211 -0
- EvoScientist/skills/flash-attention/SKILL.md +367 -0
- EvoScientist/skills/flash-attention/references/benchmarks.md +215 -0
- EvoScientist/skills/flash-attention/references/transformers-integration.md +293 -0
- EvoScientist/skills/langgraph-docs/SKILL.md +36 -0
- EvoScientist/skills/llama-cpp/SKILL.md +258 -0
- EvoScientist/skills/llama-cpp/references/optimization.md +89 -0
- EvoScientist/skills/llama-cpp/references/quantization.md +213 -0
- EvoScientist/skills/llama-cpp/references/server.md +125 -0
- EvoScientist/skills/lm-evaluation-harness/SKILL.md +490 -0
- EvoScientist/skills/lm-evaluation-harness/references/api-evaluation.md +490 -0
- EvoScientist/skills/lm-evaluation-harness/references/benchmark-guide.md +488 -0
- EvoScientist/skills/lm-evaluation-harness/references/custom-tasks.md +602 -0
- EvoScientist/skills/lm-evaluation-harness/references/distributed-eval.md +519 -0
- EvoScientist/skills/ml-paper-writing/SKILL.md +937 -0
- EvoScientist/skills/ml-paper-writing/references/checklists.md +361 -0
- EvoScientist/skills/ml-paper-writing/references/citation-workflow.md +562 -0
- EvoScientist/skills/ml-paper-writing/references/reviewer-guidelines.md +367 -0
- EvoScientist/skills/ml-paper-writing/references/sources.md +159 -0
- EvoScientist/skills/ml-paper-writing/references/writing-guide.md +476 -0
- EvoScientist/skills/ml-paper-writing/templates/README.md +251 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/README.md +534 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026-unified-template.tex +952 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bib +111 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.bst +1493 -0
- EvoScientist/skills/ml-paper-writing/templates/aaai2026/aaai2026.sty +315 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/README.md +50 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/acl.sty +312 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/acl_latex.tex +377 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/acl_lualatex.tex +101 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/acl_natbib.bst +1940 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/anthology.bib.txt +26 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/custom.bib +70 -0
- EvoScientist/skills/ml-paper-writing/templates/acl/formatting.md +326 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/README.md +3 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bib +11 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.bst +1440 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.sty +218 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/colm2025_conference.tex +305 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/fancyhdr.sty +485 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/math_commands.tex +508 -0
- EvoScientist/skills/ml-paper-writing/templates/colm2025/natbib.sty +1246 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/fancyhdr.sty +485 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bib +24 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.bst +1440 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.sty +246 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/iclr2026_conference.tex +414 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/math_commands.tex +508 -0
- EvoScientist/skills/ml-paper-writing/templates/iclr2026/natbib.sty +1246 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithm.sty +79 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/algorithmic.sty +201 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.bib +75 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/example_paper.tex +662 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/fancyhdr.sty +864 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.bst +1443 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/icml2026.sty +767 -0
- EvoScientist/skills/ml-paper-writing/templates/icml2026/icml_numpapers.pdf +0 -0
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/Makefile +36 -0
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/extra_pkgs.tex +53 -0
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/main.tex +38 -0
- EvoScientist/skills/ml-paper-writing/templates/neurips2025/neurips.sty +382 -0
- EvoScientist/skills/peft/SKILL.md +431 -0
- EvoScientist/skills/peft/references/advanced-usage.md +514 -0
- EvoScientist/skills/peft/references/troubleshooting.md +480 -0
- EvoScientist/skills/ray-data/SKILL.md +326 -0
- EvoScientist/skills/ray-data/references/integration.md +82 -0
- EvoScientist/skills/ray-data/references/transformations.md +83 -0
- EvoScientist/skills/skill-creator/LICENSE.txt +202 -0
- EvoScientist/skills/skill-creator/SKILL.md +356 -0
- EvoScientist/skills/skill-creator/references/output-patterns.md +82 -0
- EvoScientist/skills/skill-creator/references/workflows.md +28 -0
- EvoScientist/skills/skill-creator/scripts/init_skill.py +303 -0
- EvoScientist/skills/skill-creator/scripts/package_skill.py +110 -0
- EvoScientist/skills/skill-creator/scripts/quick_validate.py +95 -0
- EvoScientist/skills/tensorboard/SKILL.md +629 -0
- EvoScientist/skills/tensorboard/references/integrations.md +638 -0
- EvoScientist/skills/tensorboard/references/profiling.md +545 -0
- EvoScientist/skills/tensorboard/references/visualization.md +620 -0
- EvoScientist/skills/vllm/SKILL.md +364 -0
- EvoScientist/skills/vllm/references/optimization.md +226 -0
- EvoScientist/skills/vllm/references/quantization.md +284 -0
- EvoScientist/skills/vllm/references/server-deployment.md +255 -0
- EvoScientist/skills/vllm/references/troubleshooting.md +447 -0
- {evoscientist-0.1.0rc1.dist-info → evoscientist-0.1.0rc2.dist-info}/METADATA +26 -3
- evoscientist-0.1.0rc2.dist-info/RECORD +119 -0
- evoscientist-0.1.0rc1.dist-info/RECORD +0 -21
- {evoscientist-0.1.0rc1.dist-info → evoscientist-0.1.0rc2.dist-info}/WHEEL +0 -0
- {evoscientist-0.1.0rc1.dist-info → evoscientist-0.1.0rc2.dist-info}/entry_points.txt +0 -0
- {evoscientist-0.1.0rc1.dist-info → evoscientist-0.1.0rc2.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
77
|
+
main_tracker = ToolCallTracker()
|
|
78
78
|
full_response = ""
|
|
79
79
|
|
|
80
|
-
# Track sub-agent names
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
86
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
179
|
-
if
|
|
180
|
-
|
|
181
|
-
|
|
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,
|
|
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
|
|
209
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
423
|
-
self._subagent_map[name]
|
|
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
|
-
|
|
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":
|
|
451
|
-
"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":
|
|
470
|
-
"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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
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
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
|
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-
|
|
914
|
+
compact: If True, render minimal 1-line summary (completed sub-agents)
|
|
709
915
|
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
#
|
|
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("
|
|
737
|
-
line.append(
|
|
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("
|
|
740
|
-
line.append(
|
|
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("
|
|
962
|
+
header.append("\u250c ", style=BORDER)
|
|
762
963
|
if sa.is_active:
|
|
763
|
-
header.append(
|
|
964
|
+
header.append(f"\u25b6 {display_name}", style="bold cyan")
|
|
764
965
|
else:
|
|
765
|
-
header.append(
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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"
|
|
1000
|
+
footer = Text(f"\u2514 done ({total} tools)", style="dim green")
|
|
805
1001
|
elements.append(footer)
|
|
806
1002
|
elif valid_calls:
|
|
807
|
-
footer = Text("
|
|
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
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
869
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
#
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
903
|
-
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
-
#
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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('<
|
|
1394
|
+
HTML('<ansiblue><b>></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.
|
|
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]")
|