pygpt-net 2.6.62__py3-none-any.whl → 2.6.64__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 (74) hide show
  1. pygpt_net/CHANGELOG.txt +11 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/attachment/attachment.py +17 -8
  4. pygpt_net/controller/camera/camera.py +4 -4
  5. pygpt_net/controller/lang/custom.py +2 -2
  6. pygpt_net/controller/presets/editor.py +65 -1
  7. pygpt_net/controller/ui/mode.py +18 -3
  8. pygpt_net/core/agents/custom/llama_index/runner.py +15 -52
  9. pygpt_net/core/agents/custom/runner.py +194 -76
  10. pygpt_net/core/agents/runners/llama_workflow.py +60 -10
  11. pygpt_net/core/render/web/renderer.py +11 -0
  12. pygpt_net/data/config/config.json +3 -3
  13. pygpt_net/data/config/models.json +3 -3
  14. pygpt_net/data/config/presets/agent_openai_b2b.json +1 -15
  15. pygpt_net/data/config/presets/agent_openai_coder.json +0 -0
  16. pygpt_net/data/config/presets/agent_openai_evolve.json +1 -23
  17. pygpt_net/data/config/presets/agent_openai_planner.json +1 -21
  18. pygpt_net/data/config/presets/agent_openai_researcher.json +1 -21
  19. pygpt_net/data/config/presets/agent_openai_supervisor.json +1 -13
  20. pygpt_net/data/config/presets/agent_openai_writer.json +1 -15
  21. pygpt_net/data/config/presets/agent_supervisor.json +1 -11
  22. pygpt_net/data/js/app/runtime.js +10 -0
  23. pygpt_net/data/js/app/scroll.js +14 -0
  24. pygpt_net/data/js/app.min.js +6 -4
  25. pygpt_net/data/locale/locale.de.ini +32 -0
  26. pygpt_net/data/locale/locale.en.ini +37 -0
  27. pygpt_net/data/locale/locale.es.ini +32 -0
  28. pygpt_net/data/locale/locale.fr.ini +32 -0
  29. pygpt_net/data/locale/locale.it.ini +32 -0
  30. pygpt_net/data/locale/locale.pl.ini +34 -2
  31. pygpt_net/data/locale/locale.uk.ini +32 -0
  32. pygpt_net/data/locale/locale.zh.ini +32 -0
  33. pygpt_net/js_rc.py +7571 -7499
  34. pygpt_net/provider/agents/base.py +0 -0
  35. pygpt_net/provider/agents/llama_index/flow_from_schema.py +0 -0
  36. pygpt_net/provider/agents/llama_index/planner_workflow.py +15 -3
  37. pygpt_net/provider/agents/llama_index/workflow/codeact.py +0 -0
  38. pygpt_net/provider/agents/llama_index/workflow/planner.py +272 -44
  39. pygpt_net/provider/agents/llama_index/workflow/supervisor.py +0 -0
  40. pygpt_net/provider/agents/openai/agent.py +0 -0
  41. pygpt_net/provider/agents/openai/agent_b2b.py +4 -4
  42. pygpt_net/provider/agents/openai/agent_planner.py +631 -254
  43. pygpt_net/provider/agents/openai/agent_with_experts.py +0 -0
  44. pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +4 -4
  45. pygpt_net/provider/agents/openai/agent_with_feedback.py +4 -4
  46. pygpt_net/provider/agents/openai/evolve.py +6 -9
  47. pygpt_net/provider/agents/openai/flow_from_schema.py +0 -0
  48. pygpt_net/provider/agents/openai/supervisor.py +290 -37
  49. pygpt_net/provider/api/google/__init__.py +9 -3
  50. pygpt_net/provider/api/google/image.py +11 -1
  51. pygpt_net/provider/api/google/music.py +375 -0
  52. pygpt_net/provider/api/x_ai/__init__.py +0 -0
  53. pygpt_net/provider/core/agent/__init__.py +0 -0
  54. pygpt_net/provider/core/agent/base.py +0 -0
  55. pygpt_net/provider/core/agent/json_file.py +0 -0
  56. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +0 -0
  57. pygpt_net/provider/llms/base.py +0 -0
  58. pygpt_net/provider/llms/deepseek_api.py +0 -0
  59. pygpt_net/provider/llms/google.py +0 -0
  60. pygpt_net/provider/llms/hugging_face_api.py +0 -0
  61. pygpt_net/provider/llms/hugging_face_router.py +0 -0
  62. pygpt_net/provider/llms/mistral.py +0 -0
  63. pygpt_net/provider/llms/perplexity.py +0 -0
  64. pygpt_net/provider/llms/x_ai.py +0 -0
  65. pygpt_net/ui/widget/dialog/confirm.py +34 -8
  66. pygpt_net/ui/widget/option/combo.py +149 -11
  67. pygpt_net/ui/widget/textarea/input.py +1 -1
  68. pygpt_net/ui/widget/textarea/web.py +1 -1
  69. pygpt_net/ui/widget/vision/camera.py +135 -12
  70. {pygpt_net-2.6.62.dist-info → pygpt_net-2.6.64.dist-info}/METADATA +13 -2
  71. {pygpt_net-2.6.62.dist-info → pygpt_net-2.6.64.dist-info}/RECORD +53 -52
  72. {pygpt_net-2.6.62.dist-info → pygpt_net-2.6.64.dist-info}/LICENSE +0 -0
  73. {pygpt_net-2.6.62.dist-info → pygpt_net-2.6.64.dist-info}/WHEEL +0 -0
  74. {pygpt_net-2.6.62.dist-info → pygpt_net-2.6.64.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,14 @@
1
+ 2.6.64 (2025-09-27)
2
+
3
+ - Added translations to agent headers.
4
+ - Improved presets tabs.
5
+ - Added support for music (Lyria) in both image and video modes (beta).
6
+
7
+ 2.6.63 (2025-09-27)
8
+
9
+ - Improved agents' workflows.
10
+ - Enhanced the display of agents' steps in the UI.
11
+
1
12
  2.6.62 (2025-09-26)
2
13
 
3
14
  - Enhanced agent workflow execution.
pygpt_net/__init__.py CHANGED
@@ -6,15 +6,15 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.26 00:00:00 #
9
+ # Updated Date: 2025.09.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.6.62"
17
- __build__ = "2025-09-26"
16
+ __version__ = "2.6.64"
17
+ __build__ = "2025-09-27"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
20
20
  __report__ = "https://github.com/szczyglis-dev/py-gpt/issues"
@@ -579,19 +579,28 @@ class Attachment:
579
579
  if not os.path.exists(url):
580
580
  return
581
581
 
582
- if not all:
583
- image_ext = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff']
584
- ext = os.path.splitext(url)[1].lower()
585
- if ext not in image_ext:
586
- return
582
+ is_image = False
583
+ image_ext = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff']
584
+ ext = os.path.splitext(url)[1].lower()
585
+ if ext in image_ext:
586
+ is_image = True
587
+
588
+ if not all and not is_image:
589
+ return
590
+
591
+ if is_image:
592
+ title = "attachments.paste.img"
593
+ status = "painter.capture.manual.captured.success"
594
+ else:
595
+ title = "attachments.paste.file"
596
+ status = "attachments.paste.success"
587
597
 
588
598
  mode = self.window.core.config.get('mode')
589
- title = "Clipboard image"
590
- self.window.core.attachments.new(mode, title, url, False)
599
+ self.window.core.attachments.new(mode, trans(title), url, False)
591
600
  self.window.core.attachments.save()
592
601
  self.window.controller.attachment.update()
593
602
  event = KernelEvent(KernelEvent.STATUS, {
594
- 'status': trans("painter.capture.manual.captured.success") + ' ' + os.path.basename(url),
603
+ 'status': trans(status) + ' ' + os.path.basename(url),
595
604
  })
596
605
  self.window.dispatch(event)
597
606
 
@@ -67,9 +67,9 @@ class Camera(QObject):
67
67
 
68
68
  # update label
69
69
  if not self.window.core.config.get('vision.capture.auto'):
70
- self.window.ui.nodes['video.preview'].label.setText(trans("vision.capture.label"))
70
+ self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.label"))
71
71
  else:
72
- self.window.ui.nodes['video.preview'].label.setText(trans("vision.capture.auto.label"))
72
+ self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.auto.label"))
73
73
 
74
74
  def update(self):
75
75
  """Update camera frame"""
@@ -381,7 +381,7 @@ class Camera(QObject):
381
381
  {'value': True}
382
382
  )
383
383
  """
384
- self.window.ui.nodes['video.preview'].label.setText(trans("vision.capture.auto.label"))
384
+ self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.auto.label"))
385
385
 
386
386
  if not self.window.core.config.get('vision.capture.enabled'):
387
387
  self.enable_capture()
@@ -403,7 +403,7 @@ class Camera(QObject):
403
403
  {'value': False}
404
404
  )
405
405
  """
406
- self.window.ui.nodes['video.preview'].label.setText(trans("vision.capture.label"))
406
+ self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.label"))
407
407
 
408
408
  def toggle_auto(self, state: bool):
409
409
  """
@@ -68,9 +68,9 @@ class Custom:
68
68
 
69
69
  # camera capture
70
70
  if not self.window.core.config.get('vision.capture.auto'):
71
- self.window.ui.nodes['video.preview'].label.setText(trans("vision.capture.label"))
71
+ self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.label"))
72
72
  else:
73
- self.window.ui.nodes['video.preview'].label.setText(trans("vision.capture.auto.label"))
73
+ self.window.ui.nodes['video.preview'].video.setToolTip(trans("vision.capture.auto.label"))
74
74
 
75
75
  # files / indexes
76
76
  self.window.ui.nodes['output_files'].btn_upload.setText(trans('files.local.upload'))
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.24 00:00:00 #
9
+ # Updated Date: 2025.09.27 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -393,6 +393,9 @@ class Editor:
393
393
  value=extra_options[key].get('default', None),
394
394
  )
395
395
 
396
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
397
+ self._apply_combo_defaults_for_group(option_key, extra_options)
398
+
396
399
  def load_extra_defaults(self):
397
400
  """Load extra options defaults for preset editor"""
398
401
  if not self.tab_options_idx:
@@ -423,6 +426,8 @@ class Editor:
423
426
  option=extra_options[key],
424
427
  value=value,
425
428
  )
429
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
430
+ self._apply_combo_defaults_for_group(option_key, extra_options)
426
431
 
427
432
  def load_extra_defaults_current(self):
428
433
  """Load extra options defaults on mode change"""
@@ -479,6 +484,8 @@ class Editor:
479
484
  option=extra_options[key],
480
485
  value=value,
481
486
  )
487
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
488
+ self._apply_combo_defaults_for_group(option_key, extra_options)
482
489
 
483
490
  def append_extra_options(self, preset: PresetItem):
484
491
  """
@@ -785,6 +792,9 @@ class Editor:
785
792
  value=opt_schema.get('default'),
786
793
  )
787
794
 
795
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
796
+ self._apply_combo_defaults_for_group(config_id, schema_options)
797
+
788
798
  # 4) Recompute mapping fully based on actual tabs and their 'agent_id' properties.
789
799
  self._rebuild_tab_index_mapping()
790
800
 
@@ -1521,6 +1531,9 @@ class Editor:
1521
1531
  value=opt_schema.get('default'),
1522
1532
  )
1523
1533
 
1534
+ # ensure combo defaults are effectively applied for this tab (only empty values are updated)
1535
+ self._apply_combo_defaults_for_group(config_id, schema_options)
1536
+
1524
1537
  # 7) Recompute the index mapping strictly from the QTabWidget
1525
1538
  self._rebuild_tab_index_mapping()
1526
1539
 
@@ -1529,3 +1542,54 @@ class Editor:
1529
1542
 
1530
1543
  finally:
1531
1544
  tabs.setUpdatesEnabled(True)
1545
+
1546
+ # ---------- Helpers for reliable combo defaults in agent extra options ----------
1547
+
1548
+ def _apply_combo_defaults_for_group(self, parent_id: str, schema_options: Dict[str, Any]) -> None:
1549
+ """
1550
+ Ensure that combo-type inputs inside a given UI config group have their default values applied
1551
+ when the current value is empty ("", None or "_"). This avoids the situation where combo boxes
1552
+ remain uninitialized while other field types receive defaults correctly.
1553
+
1554
+ This function never overrides a non-empty value set by the user or loaded from a preset.
1555
+ """
1556
+ if not schema_options:
1557
+ return
1558
+
1559
+ get_value = self.window.controller.config.get_value
1560
+ apply_value = self.window.controller.config.apply_value
1561
+
1562
+ for key, opt_schema in schema_options.items():
1563
+ if not isinstance(opt_schema, dict):
1564
+ continue
1565
+ if opt_schema.get('type') != 'combo':
1566
+ continue
1567
+
1568
+ default_val = opt_schema.get('default', None)
1569
+ if default_val is None:
1570
+ continue
1571
+
1572
+ current_val = get_value(
1573
+ parent_id=parent_id,
1574
+ key=key,
1575
+ option=opt_schema,
1576
+ )
1577
+
1578
+ # Treat "_", "", None as empty and safe to replace with default
1579
+ if current_val in (None, "", "_"):
1580
+ # First try apply_value (standard path)
1581
+ apply_value(
1582
+ parent_id=parent_id,
1583
+ key=key,
1584
+ option=opt_schema,
1585
+ value=default_val,
1586
+ )
1587
+ # Additionally set directly on widget if accessible to guard against timing of key population
1588
+ try:
1589
+ widget_group = self.window.ui.config.get(parent_id, {})
1590
+ widget = widget_group.get(key)
1591
+ if widget and hasattr(widget, "set_value"):
1592
+ widget.set_value(default_val)
1593
+ except Exception:
1594
+ # Silent fallback; apply_value above should already handle most cases
1595
+ pass
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.22 12:00:00 #
9
+ # Updated Date: 2025.09.27 15:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.core.types import (
@@ -58,6 +58,19 @@ class Mode:
58
58
  is_completion = mode == MODE_COMPLETION
59
59
  is_audio = mode == MODE_AUDIO
60
60
 
61
+ # enable/disable system prompt edit - disable in agents (prompts are defined per agent in presets)
62
+ if not is_agent_openai and not is_agent_llama:
63
+ presets_editor.toggle_tab("personalize", True)
64
+ if 'preset.prompt' in ui_nodes and ui_nodes['preset.prompt'].isReadOnly():
65
+ ui_nodes['preset.prompt'].setReadOnly(False)
66
+ ui_nodes['preset.prompt'].setPlaceholderText("")
67
+ else:
68
+ presets_editor.toggle_tab("personalize", False)
69
+ if 'preset.prompt' in ui_nodes and not ui_nodes['preset.prompt'].isReadOnly():
70
+ ui_nodes['preset.prompt'].setReadOnly(True)
71
+ ui_nodes['preset.prompt'].setPlaceholderText(trans("toolbox.agent.preset.placeholder"))
72
+
73
+ # audio options visibility
61
74
  if not is_audio:
62
75
  ui_nodes['audio.auto_turn'].setVisible(False)
63
76
  ui_nodes["audio.loop"].setVisible(False)
@@ -71,6 +84,7 @@ class Mode:
71
84
  else:
72
85
  ctrl.audio.toggle_output_icon(False)
73
86
 
87
+ # presets/assistants visibility
74
88
  if not is_assistant:
75
89
  ui_nodes['presets.widget'].setVisible(True)
76
90
  else:
@@ -81,6 +95,7 @@ class Mode:
81
95
  else:
82
96
  ui_nodes['env.widget'].setVisible(True)
83
97
 
98
+ # agents/experts/presets label visibility
84
99
  show_agents_label = is_agent or is_agent_llama or is_agent_openai
85
100
  if show_agents_label:
86
101
  ui_nodes['preset.agents.label'].setVisible(True)
@@ -112,6 +127,7 @@ class Mode:
112
127
  else:
113
128
  ui_nodes['preset.editor.agent_provider_openai'].setVisible(False)
114
129
 
130
+ # prompt editor toolbox visibility
115
131
  if is_agent:
116
132
  presets_editor.toggle_tab("experts", True)
117
133
  ui_nodes['preset.editor.temperature'].setVisible(True)
@@ -145,6 +161,7 @@ class Mode:
145
161
  ui_nodes['preset.editor.modes'].setVisible(True)
146
162
  ui_tabs['preset.editor.extra'].setTabText(0, trans("preset.prompt"))
147
163
 
164
+ # image options visibility
148
165
  if is_image:
149
166
  ui_nodes['media.raw'].setVisible(True)
150
167
  if ctrl.media.is_video_model():
@@ -198,10 +215,8 @@ class Mode:
198
215
  # remote tools icon visibility
199
216
  if not is_image and not is_completion:
200
217
  self.window.ui.nodes['input'].set_icon_visible("web", True)
201
- # ui_nodes['icon.remote_tool.web'].setVisible(True)
202
218
  else:
203
219
  self.window.ui.nodes['input'].set_icon_visible("web", False)
204
- # ui_nodes['icon.remote_tool.web'].setVisible(False)
205
220
 
206
221
  ui_tabs['input'].setTabVisible(2, is_assistant)
207
222
  ui_tabs['input'].setTabVisible(3, (not is_assistant) and (not is_image))
@@ -1,3 +1,5 @@
1
+ # core/agents/runners/llama_workflow.py
2
+
1
3
  #!/usr/bin/env python3
2
4
  # -*- coding: utf-8 -*-
3
5
  # ================================================== #
@@ -6,7 +8,7 @@
6
8
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
9
  # MIT License #
8
10
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.26 17:00:00 #
11
+ # Updated Date: 2025.09.27 06:00:00 #
10
12
  # ================================================== #
11
13
 
12
14
  from __future__ import annotations
@@ -207,37 +209,6 @@ class DynamicFlowWorkflowLI(Workflow):
207
209
  return False
208
210
  return False
209
211
 
210
- def _friendly_map(self) -> Dict[str, str]:
211
- return {aid: a.name or aid for aid, a in self.fs.agents.items()}
212
-
213
- def _friendly_map_for_routes(self, route_ids: List[str]) -> Dict[str, Any]:
214
- """
215
- Build a friendly map for the given route ids:
216
- - Always include a human-friendly name.
217
- - Include role only if provided in preset options or schema and non-empty.
218
- """
219
- out: Dict[str, Any] = {}
220
- for rid in route_ids or []:
221
- a = self.fs.agents.get(rid)
222
- name = (a.name if a and a.name else rid)
223
- # Prefer preset option, then schema role
224
- role_opt = None
225
- try:
226
- role_opt = self.option_get(rid, "role", None)
227
- except Exception:
228
- role_opt = None
229
- role_schema = getattr(a, "role", None) if a is not None else None
230
- role_val = None
231
- if isinstance(role_opt, str) and role_opt.strip():
232
- role_val = role_opt.strip()
233
- elif isinstance(role_schema, str) and role_schema.strip():
234
- role_val = role_schema.strip()
235
- item = {"name": name}
236
- if role_val:
237
- item["role"] = role_val
238
- out[rid] = item
239
- return out
240
-
241
212
  async def _emit(self, ctx: Context, ev: Any):
242
213
  if self.dbg.event_echo:
243
214
  self.logger.debug(f"[event] emit {ev.__class__.__name__}")
@@ -245,8 +216,8 @@ class DynamicFlowWorkflowLI(Workflow):
245
216
 
246
217
  async def _emit_agent_text(self, ctx: Context, text: str, agent_name: str = "Agent"):
247
218
  """
248
- Emit AgentStream(delta=text) robustly. If your env requires extra fields,
249
- fall back to extended AgentStream like in your SupervisorWorkflow.
219
+ Emit AgentStream(delta=text) robustly. If env requires extra fields,
220
+ fall back to extended AgentStream.
250
221
  """
251
222
  try:
252
223
  if self.dbg.event_echo:
@@ -266,16 +237,11 @@ class DynamicFlowWorkflowLI(Workflow):
266
237
  )
267
238
 
268
239
  async def _emit_header(self, ctx: Context, name: str):
269
- if self.dbg.event_echo:
270
- self.logger.debug(f"[event] header emit begin name='{name}'")
240
+ # Lightweight header to ensure agent name is known before tokens.
271
241
  await self._emit_agent_text(ctx, "", agent_name=name)
272
- # await self._emit_agent_text(ctx, f"\n\n**{name}**\n\n", agent_name=name)
273
- if self.dbg.event_echo:
274
- self.logger.debug("[event] header emit done")
275
242
 
276
243
  async def _emit_step_sep(self, ctx: Context, node_id: str):
277
244
  try:
278
- # Include human-friendly agent name in StepEvent meta for downstream ctx propagation.
279
245
  a = self.fs.agents.get(node_id)
280
246
  friendly_name = (a.name if a and a.name else node_id)
281
247
  await self._emit(
@@ -350,6 +316,9 @@ class DynamicFlowWorkflowLI(Workflow):
350
316
  return user_msg, [], "no-mem:last_output"
351
317
 
352
318
  async def _update_memory_after_step(self, node_id: str, user_msg_text: str, display_text: str):
319
+ """
320
+ Update per-node memory after a step, storing baton user message and assistant output.
321
+ """
353
322
  mem_id = self.g.agent_to_memory.get(node_id)
354
323
  mem_state = self.mem.get(mem_id) if mem_id else None
355
324
  if not mem_state:
@@ -374,7 +343,7 @@ class DynamicFlowWorkflowLI(Workflow):
374
343
  # ============== Workflow steps ==============
375
344
 
376
345
  def run(self, query: str, ctx: Optional[Context] = None, memory: Any = None, verbose: bool = False, on_stop=None):
377
- """Entry point used by your LlamaWorkflow runner."""
346
+ """Entry point used by LlamaWorkflow runner."""
378
347
  self._on_stop = on_stop
379
348
 
380
349
  # Build initial chat once
@@ -444,8 +413,9 @@ class DynamicFlowWorkflowLI(Workflow):
444
413
  return FlowTickEvent() if self._current_ids else FlowStopEvent(final_answer=self._last_plain_output or "")
445
414
 
446
415
  node: AgentNode = self.fs.agents[current_id]
447
- if self._steps > 1:
448
- await self._emit_step_sep(ctx, current_id)
416
+
417
+ # IMPORTANT: emit StepEvent also for the very first agent step.
418
+ await self._emit_step_sep(ctx, current_id)
449
419
  await self._emit_header(ctx, node.name or current_id)
450
420
 
451
421
  # Resolve runtime + per-node LLM/tools
@@ -474,11 +444,10 @@ class DynamicFlowWorkflowLI(Workflow):
474
444
  f"user='{ellipsize(user_msg_text, self.dbg.preview_chars)}'"
475
445
  )
476
446
 
477
- # Prepare friendly map with optional roles for this node's allowed routes
447
+ # Build agent
478
448
  allowed_routes_now = list(node.outputs or [])
479
- friendly_map = self._friendly_map_for_routes(allowed_routes_now)
449
+ friendly_map = {rid: self.fs.agents.get(rid).name or rid for rid in allowed_routes_now if rid in self.fs.agents}
480
450
 
481
- # Build agent (chat_history/max_iterations in ctor – best practice)
482
451
  built = self.factory.build(
483
452
  node=node,
484
453
  node_runtime=node_rt,
@@ -528,9 +497,6 @@ class DynamicFlowWorkflowLI(Workflow):
528
497
  display_text = decision.content or ""
529
498
  if display_text:
530
499
  await self._emit_agent_text(ctx, display_text, agent_name=(node.name or current_id))
531
- if self.dbg.log_memory_dump:
532
- self.logger.debug(f"[mem.prep] node={current_id} save user='{ellipsize(user_msg_text, self.dbg.preview_chars)}' "
533
- f"assist='{ellipsize(display_text, self.dbg.preview_chars)}'")
534
500
  await self._update_memory_after_step(current_id, user_msg_text, display_text)
535
501
  next_id = decision.route if decision.valid else (allowed_routes[0] if allowed_routes else None)
536
502
  if self.dbg.log_routes:
@@ -541,9 +507,6 @@ class DynamicFlowWorkflowLI(Workflow):
541
507
  display_text = raw_text_clean or ""
542
508
  if display_text:
543
509
  await self._emit_agent_text(ctx, display_text, agent_name=(node.name or current_id))
544
- if self.dbg.log_memory_dump:
545
- self.logger.debug(f"[mem.prep] node={current_id} save user='{ellipsize(user_msg_text, self.dbg.preview_chars)}' "
546
- f"assist='{ellipsize(display_text, self.dbg.preview_chars)}'")
547
510
  await self._update_memory_after_step(current_id, user_msg_text, display_text)
548
511
  outs = self.g.get_next(current_id)
549
512
  next_id = outs[0] if outs else self.g.first_connected_end(current_id)