execweave 0.8.31__tar.gz → 0.8.32__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. {execweave-0.8.31 → execweave-0.8.32}/PKG-INFO +1 -1
  2. {execweave-0.8.31 → execweave-0.8.32}/pyproject.toml +2 -2
  3. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/__init__.py +1 -1
  4. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_preview_generic.py +9 -2
  5. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/framework_adapters/camel.py +6 -0
  6. execweave-0.8.32/src/execweave/framework_adapters/camel_channel.py +71 -0
  7. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/framework_adapters/metagpt.py +15 -0
  8. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_agent_panel.py +38 -5
  9. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_execution_flow.py +1 -0
  10. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_execution_flow_provider_normalization.py +3 -1
  11. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_execution_flow_safety.py +5 -20
  12. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_flow_canvas.py +11 -0
  13. execweave-0.8.32/src/execweave/viewer_framework_flow.py +54 -0
  14. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_semantic_projection.py +5 -1
  15. execweave-0.8.32/src/execweave/viewer_session_flow.py +166 -0
  16. {execweave-0.8.31 → execweave-0.8.32}/.gitignore +0 -0
  17. {execweave-0.8.31 → execweave-0.8.32}/LICENSE +0 -0
  18. {execweave-0.8.31 → execweave-0.8.32}/README.md +0 -0
  19. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/__main__.py +0 -0
  20. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/_conversation_records_core.py +0 -0
  21. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/_dashboard_shell_base.py +0 -0
  22. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/_http_proxy_base.py +0 -0
  23. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/_http_proxy_bounded.py +0 -0
  24. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/_http_proxy_stage.py +0 -0
  25. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/agent_bootstrap.py +0 -0
  26. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/agent_topology.py +0 -0
  27. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/agent_trace.py +0 -0
  28. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/agy_preview_sanitize.py +0 -0
  29. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/analysis.py +0 -0
  30. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/anthropic.py +0 -0
  31. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/anthropic_cli.py +0 -0
  32. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/anthropic_full_fidelity.py +0 -0
  33. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/antigravity_adapter.py +0 -0
  34. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/antigravity_adapter_base.py +0 -0
  35. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/antigravity_full_fidelity.py +0 -0
  36. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/antigravity_full_fidelity_base.py +0 -0
  37. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/antigravity_full_fidelity_collaboration_base.py +0 -0
  38. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/antigravity_hook_cli.py +0 -0
  39. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/antigravity_record.py +0 -0
  40. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/antigravity_subagent_linkage.py +0 -0
  41. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/antigravity_trace_capability.py +0 -0
  42. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/auto_specialized.py +0 -0
  43. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/backends.py +0 -0
  44. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/benchmark.py +0 -0
  45. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/claude_adapter.py +0 -0
  46. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/claude_child_transcript.py +0 -0
  47. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/claude_delegation.py +0 -0
  48. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/claude_full_fidelity.py +0 -0
  49. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/claude_hook_cli.py +0 -0
  50. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/claude_hook_contract.py +0 -0
  51. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/claude_model_observer.py +0 -0
  52. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/claude_record.py +0 -0
  53. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/cli.py +0 -0
  54. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_adapter.py +0 -0
  55. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_conversation.py +0 -0
  56. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_full_fidelity.py +0 -0
  57. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_hook_cli.py +0 -0
  58. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_hook_entry.py +0 -0
  59. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_hook_lifecycle.py +0 -0
  60. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_message_diagnostics.py +0 -0
  61. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_message_transport_diagnostics.py +0 -0
  62. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_record.py +0 -0
  63. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_rollout_structures.py +0 -0
  64. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_rollout_trace.py +0 -0
  65. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/codex_rollout_trace_base.py +0 -0
  66. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/collector.py +0 -0
  67. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/command.py +0 -0
  68. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/content_evidence.py +0 -0
  69. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/content_store.py +0 -0
  70. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_archive.py +0 -0
  71. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_message_identity.py +0 -0
  72. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_preview.py +0 -0
  73. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_preview_antigravity.py +0 -0
  74. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_preview_claude.py +0 -0
  75. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_preview_codex.py +0 -0
  76. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_preview_common.py +0 -0
  77. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_preview_lines.py +0 -0
  78. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_preview_transcript.py +0 -0
  79. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_records.py +0 -0
  80. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_records_antigravity.py +0 -0
  81. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_records_codex.py +0 -0
  82. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_records_common.py +0 -0
  83. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/conversation_records_ollama.py +0 -0
  84. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/correlation.py +0 -0
  85. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/cursor_adapter.py +0 -0
  86. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/cursor_delegation.py +0 -0
  87. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/cursor_delegation_base.py +0 -0
  88. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/cursor_full_fidelity.py +0 -0
  89. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/cursor_hook_cli.py +0 -0
  90. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/cursor_hook_contract.py +0 -0
  91. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/cursor_lifecycle.py +0 -0
  92. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/cursor_record.py +0 -0
  93. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/dashboard_shell.py +0 -0
  94. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/entry.py +0 -0
  95. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/evidence_availability.py +0 -0
  96. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/evidence_grade.py +0 -0
  97. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/fidelity.py +0 -0
  98. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/filesystem.py +0 -0
  99. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/focus.py +0 -0
  100. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/framework_adapters/__init__.py +0 -0
  101. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/framework_adapters/autogen.py +0 -0
  102. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/framework_adapters/base.py +0 -0
  103. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/framework_adapters/registry.py +0 -0
  104. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/graph.py +0 -0
  105. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/graph_ops.py +0 -0
  106. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/http_proxy.py +0 -0
  107. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/http_proxy_cli.py +0 -0
  108. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/inference_gateway.py +0 -0
  109. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/inference_gateway_cli.py +0 -0
  110. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/inference_gateway_full_fidelity.py +0 -0
  111. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/inference_identity.py +0 -0
  112. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/inference_identity_cli.py +0 -0
  113. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/integrity.py +0 -0
  114. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/integrity_cli.py +0 -0
  115. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/litellm_callback.py +0 -0
  116. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/litellm_callback_cli.py +0 -0
  117. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live.py +0 -0
  118. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_core.py +0 -0
  119. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view.py +0 -0
  120. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view_extra_style.py +0 -0
  121. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view_markup.py +0 -0
  122. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view_process_layout.py +0 -0
  123. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view_readability.py +0 -0
  124. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view_script_a.py +0 -0
  125. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view_script_b.py +0 -0
  126. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view_script_c.py +0 -0
  127. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view_script_d.py +0 -0
  128. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/live_view_style.py +0 -0
  129. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/model_runtime.py +0 -0
  130. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/model_runtime_cli.py +0 -0
  131. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/model_runtime_full_fidelity.py +0 -0
  132. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/openai_compatible.py +0 -0
  133. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/openai_compatible_cli.py +0 -0
  134. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/openai_compatible_full_fidelity.py +0 -0
  135. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/opencode_adapter.py +0 -0
  136. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/opencode_event_contract.py +0 -0
  137. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/opencode_full_fidelity.py +0 -0
  138. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/opencode_hook_cli.py +0 -0
  139. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/opencode_plugin_cli.py +0 -0
  140. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/opencode_record.py +0 -0
  141. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/opencode_task_linkage.py +0 -0
  142. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/overhead_benchmark.py +0 -0
  143. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/provider_capability.py +0 -0
  144. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/provider_lifecycle.py +0 -0
  145. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/provider_record.py +0 -0
  146. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/rule_pack.py +0 -0
  147. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/rule_pack_cli.py +0 -0
  148. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/runtime_endpoint_availability.py +0 -0
  149. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/scalability_benchmark.py +0 -0
  150. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/schema.py +0 -0
  151. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/scope.py +0 -0
  152. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/semantic.py +0 -0
  153. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/sink.py +0 -0
  154. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/strace_backend.py +0 -0
  155. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/stream_assembly.py +0 -0
  156. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/theme.py +0 -0
  157. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/top.py +0 -0
  158. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/top_cli.py +0 -0
  159. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/validate.py +0 -0
  160. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/vendor/dagre.min.js +0 -0
  161. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/view_cli.py +0 -0
  162. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer.py +0 -0
  163. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_agent_panel_antigravity.py +0 -0
  164. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_agent_panel_claude.py +0 -0
  165. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_agent_panel_codex.py +0 -0
  166. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_agent_panel_cursor.py +0 -0
  167. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_agent_panel_default.py +0 -0
  168. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_agent_panel_ollama.py +0 -0
  169. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_agent_panel_opencode.py +0 -0
  170. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_agent_panel_registry.py +0 -0
  171. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_content_inspector.py +0 -0
  172. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_dashboard_clean.py +0 -0
  173. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_dashboard_focus.py +0 -0
  174. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_dashboard_hardening.py +0 -0
  175. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_dashboard_hardening_v2.py +0 -0
  176. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_dashboard_pr70.py +0 -0
  177. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_edge_decross.py +0 -0
  178. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_external_endpoints.py +0 -0
  179. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_flow_layout.py +0 -0
  180. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_gif.py +0 -0
  181. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_layout_geometry.py +0 -0
  182. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_layout_v2.py +0 -0
  183. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_layout_v2_detached.py +0 -0
  184. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_limits.py +0 -0
  185. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_live_layout.py +0 -0
  186. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_projection.py +0 -0
  187. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_projection_base.py +0 -0
  188. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_projection_bridges.py +0 -0
  189. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/viewer_tool_mirror_identity.py +0 -0
  190. {execweave-0.8.31 → execweave-0.8.32}/src/execweave/workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execweave
3
- Version: 0.8.31
3
+ Version: 0.8.32
4
4
  Summary: Turn AI-agent runtime behavior into a local interactive execution graph.
5
5
  Project-URL: Homepage, https://github.com/Irish-kw/ExecWeave
6
6
  Project-URL: Repository, https://github.com/Irish-kw/ExecWeave
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execweave"
7
- # ExecWeave v0.8.31 release metadata
8
- version = "0.8.31"
7
+ # ExecWeave v0.8.32 release metadata
8
+ version = "0.8.32"
9
9
  description = "Turn AI-agent runtime behavior into a local interactive execution graph."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """ExecWeave runtime graph package."""
2
2
 
3
- __version__ = "0.8.31"
3
+ __version__ = "0.8.32"
@@ -74,7 +74,7 @@ def _routed_agent_message(
74
74
  phase = value.get("phase")
75
75
  task_name = value.get("task_name")
76
76
  content_state = value.get("content_state")
77
- return [_message(
77
+ message = _message(
78
78
  timestamp=timestamp,
79
79
  ordinal=ordinal,
80
80
  kind=kind if isinstance(kind, str) and kind else "agent_message",
@@ -84,7 +84,10 @@ def _routed_agent_message(
84
84
  phase=phase if isinstance(phase, str) and phase else None,
85
85
  task_name=task_name if isinstance(task_name, str) and task_name else None,
86
86
  content_state=content_state if isinstance(content_state, str) and content_state else "plaintext",
87
- )]
87
+ )
88
+ if isinstance(value.get("message_id"), str):
89
+ message["message_id"] = value["message_id"]
90
+ return [message]
88
91
 
89
92
 
90
93
  def _generic_content_messages(
@@ -108,6 +111,10 @@ def _generic_content_messages(
108
111
  if routed:
109
112
  return routed
110
113
  if "model_request" in kind or "model_response" in kind or "model_failure" in kind:
114
+ if "model_response" in kind:
115
+ response = _response_messages(value, timestamp=timestamp, ordinal=ordinal, agent_path=agent_path)
116
+ if response:
117
+ return response
111
118
  structured = _structured_messages(value, timestamp=timestamp, ordinal=ordinal, agent_path=agent_path)
112
119
  if structured:
113
120
  return structured
@@ -140,6 +140,12 @@ class CAMELAdapter(FrameworkAdapter):
140
140
  methods[name] = _callback_method(name, self)
141
141
  return type("ExecWeaveCAMELWorkforceCallback", (base,), methods)()
142
142
 
143
+ def observe_task_channel(self, channel: Any, *, publishers: dict[str, EntityRef]) -> Any:
144
+ """Record actual task dispatch/receipt and result dispatch/receipt."""
145
+ from .camel_channel import observe_task_channel
146
+
147
+ return observe_task_channel(self, channel, publishers)
148
+
143
149
  def _callback_event(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
144
150
  payload = _payload(args, kwargs)
145
151
  if name in {"log_message", "log_stream_chunk"}:
@@ -0,0 +1,71 @@
1
+ """Observe an explicitly supplied CAMEL TaskChannel instance, not global classes."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping
5
+
6
+ from .base import EntityRef
7
+
8
+
9
+ def observe_task_channel(adapter: Any, channel: Any, publishers: Mapping[str, EntityRef]) -> Any:
10
+ if getattr(channel, "_execweave_channel_adapter", None) is adapter:
11
+ return channel
12
+ names = ("post_task", "get_assigned_task_by_assignee", "return_task", "get_returned_task_by_publisher")
13
+ originals = {name: getattr(channel, name, None) for name in names}
14
+ if not all(callable(method) for method in originals.values()):
15
+ raise TypeError("CAMEL channel does not expose the supported task delivery boundaries")
16
+ routes: dict[str, dict[str, Any]] = {}
17
+ counter = 0
18
+
19
+ def record(route, *, reply=False, received=False):
20
+ source, target = route["publisher"], route["assignee"]
21
+ task = route["task"]
22
+ content = str(getattr(task, "result", "") or "") if reply else str(getattr(task, "content", "") or "")
23
+ if reply:
24
+ source, target = target, source
25
+ message = adapter.message(
26
+ route["id"] + (":reply" if reply else ":dispatch"), source, target,
27
+ content=content, role="agent", received=received,
28
+ routing_source="camel_task_channel_delivery" if received else "camel_task_channel_post",
29
+ native_task_id=str(task.id),
30
+ )
31
+ if source is None or target is None:
32
+ adapter.message_unrouted(message, callback_name="TaskChannel", missing_sender=source is None, missing_recipient=target is None)
33
+
34
+ async def post_task(task, publisher_id, assignee_id):
35
+ nonlocal counter
36
+ result = await originals["post_task"](task, publisher_id, assignee_id)
37
+ counter += 1
38
+ route = {"id": f"channel:{task.id}:{counter}", "task": task,
39
+ "publisher": publishers.get(str(publisher_id)) or adapter._agents.get(str(publisher_id)),
40
+ "assignee": adapter._agents.get(str(assignee_id))}
41
+ routes[str(task.id)] = route
42
+ record(route)
43
+ return result
44
+
45
+ async def assigned(assignee_id):
46
+ task = await originals["get_assigned_task_by_assignee"](assignee_id)
47
+ route = routes.get(str(task.id))
48
+ if route:
49
+ record(route, received=True)
50
+ return task
51
+
52
+ async def return_task(task_id):
53
+ result = await originals["return_task"](task_id)
54
+ route = routes.get(str(task_id))
55
+ if route:
56
+ record(route, reply=True)
57
+ return result
58
+
59
+ async def returned(publisher_id):
60
+ task = await originals["get_returned_task_by_publisher"](publisher_id)
61
+ route = routes.pop(str(task.id), None)
62
+ if route:
63
+ record(route, reply=True, received=True)
64
+ return task
65
+
66
+ channel.post_task = post_task
67
+ channel.get_assigned_task_by_assignee = assigned
68
+ channel.return_task = return_task
69
+ channel.get_returned_task_by_publisher = returned
70
+ channel._execweave_channel_adapter = adapter
71
+ return channel
@@ -165,6 +165,8 @@ class MetaGPTAdapter(FrameworkAdapter):
165
165
  source_name = getattr(message_object, "sent_from", None)
166
166
  target_name = getattr(message_object, "send_to", None)
167
167
  source = self._roles.get(str(source_name)) if source_name else None
168
+ if source is None and source_name == "user":
169
+ source = self.entity("user", "user", name="user")
168
170
  content = _text(getattr(message_object, "content", None))
169
171
  role = _text(getattr(message_object, "role", None))
170
172
  common_attributes = {
@@ -222,6 +224,19 @@ class MetaGPTAdapter(FrameworkAdapter):
222
224
  )
223
225
  return message
224
226
 
227
+ def observe_message_delivery(self, message_object: Any, *, recipient: EntityRef, task: EntityRef | None = None) -> EntityRef:
228
+ """Call after the native Role.put_message accepted a message into its queue."""
229
+ self._message_counter += 1
230
+ return self.observe_message(
231
+ getattr(message_object, "id", None) or f"delivery-{self._message_counter}",
232
+ source=(self.entity("user", "user", name="user") if getattr(message_object, "sent_from", None) == "user"
233
+ else self._roles.get(str(getattr(message_object, "sent_from", "")))),
234
+ target=recipient,
235
+ content=_text(getattr(message_object, "content", None)),
236
+ role=_text(getattr(message_object, "role", None)), task=task, received=True,
237
+ routing_source="metagpt.Role.put_message",
238
+ )
239
+
225
240
  def observe_action(
226
241
  self,
227
242
  action_id: str | int,
@@ -15,8 +15,8 @@ _AGENT_PANEL_CSS = r"""
15
15
  #open-final{display:none!important}
16
16
  .execweave-agent-view{display:grid;gap:12px}
17
17
  .execweave-agent-card{border:1px solid var(--border);border-radius:10px;background:var(--panel2);overflow:hidden}
18
- .execweave-agent-label{padding:9px 11px;border-bottom:1px solid var(--border);font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}
19
- .execweave-agent-body{margin:0;padding:11px 12px;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text)}
18
+ .execweave-agent-label,.execweave-communication-label{padding:9px 11px;border-bottom:1px solid var(--border);font-size:10px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}
19
+ .execweave-agent-body,.execweave-message-body{margin:0;padding:11px 12px;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text)}
20
20
  .execweave-agent-empty{color:var(--muted);font-style:italic}
21
21
  .execweave-agent-rounds{display:grid;gap:10px}
22
22
  .execweave-agent-round{display:grid;gap:12px}
@@ -28,6 +28,8 @@ _AGENT_PANEL_CSS = r"""
28
28
  .execweave-agent-older[open]>summary{border-bottom:1px solid var(--border)}
29
29
  .execweave-agent-older>.execweave-agent-round{padding:10px 11px 11px}
30
30
  .execweave-agent-when{font-variant-numeric:tabular-nums;color:var(--text)}
31
+ .execweave-message-history{border:1px solid var(--border);border-radius:10px;background:var(--panel2);margin:6px 0}
32
+ .execweave-message-history>summary{cursor:pointer;padding:9px 11px;font-size:11px;color:var(--muted);overflow-wrap:anywhere}
31
33
  """.strip()
32
34
 
33
35
  _AGENT_PANEL_JS = r"""
@@ -119,7 +121,7 @@ function foldStateFor(node){
119
121
  function rememberVisibleFoldState(){
120
122
  if(!selectedNode)return;
121
123
  const state=foldStateFor(selectedNode);
122
- for(const fold of details.querySelectorAll('.execweave-agent-older[data-fold-key]')){
124
+ for(const fold of details.querySelectorAll('.execweave-agent-older[data-fold-key],.execweave-message-history[data-fold-key]')){
123
125
  const key=String(fold.dataset.foldKey||'');
124
126
  if(key)state.set(key,fold.open);
125
127
  }
@@ -348,6 +350,7 @@ function execweaveNodeCardsBase(node){
348
350
  return rows;
349
351
  }
350
352
  if(kind==='process'){
353
+ if(Array.isArray(a.viewer_message_evidence)&&a.viewer_message_evidence.length)add('Unrouted callback messages',`${a.viewer_message_evidence.length} observations; sender/recipient not exposed. Original evidence retained in raw graph.`);
351
354
  add('Command',a.cmdline);
352
355
  add('Executable',a.exe);
353
356
  add('Process',[a.pid&&`pid ${a.pid}`,a.ppid&&`parent ${a.ppid}`].filter(Boolean).join(' \u00b7 '));
@@ -438,8 +441,38 @@ function execweaveFillAssignedTask(round,node,isRoot){
438
441
  const value=round||{cards:[]},cards=Array.isArray(value.cards)?value.cards.map(card=>[card[0],card[1]]):[];
439
442
  const label=isRoot?'Prompt':'Task',index=cards.findIndex(card=>String(card[0]||'')===label);
440
443
  if(index<0)cards.unshift([label,task]);else if(!commandText(cards[index][1]))cards[index][1]=task;
444
+ else if(attrs(node).conversation_scope==='framework_agent'&&commandText(cards[index][1])!==task)cards.unshift(['Assigned task',task]);
441
445
  return{...value,cards};
442
446
  }
447
+ function agentCommunicationHistory(node,messages){
448
+ // Read only this exact agent's conversation record. Never sweep sibling
449
+ // transcripts into the parent merely because they share a run or a model.
450
+ const candidates=messages.filter(message=>isObserved(message)&&!isInjected(message)&&(
451
+ String(message?.kind||'')==='agent_message'||
452
+ (String(message?.sender||'').startsWith('/')&&String(message?.recipient||'').startsWith('/')&&message.sender!==message.recipient)
453
+ ));
454
+ const byMessage=new Map();
455
+ for(const message of candidates){
456
+ const key=message.message_id?JSON.stringify([message.message_id,message.sender,message.recipient,message.text]):messageKey(message);
457
+ const previous=byMessage.get(key);
458
+ if(!previous||message.phase==='received')byMessage.set(key,message);
459
+ }
460
+ const routed=[...byMessage.values()];
461
+ if(!routed.length)return null;
462
+ const section=document.createElement('section');section.className='execweave-agent-communication';
463
+ section.dataset.agentId=String(node.id);
464
+ const title=document.createElement('div');title.className='execweave-communication-label';title.textContent='Agent communication';section.appendChild(title);
465
+ const state=foldStateFor(node);
466
+ for(const message of [...routed].reverse()){
467
+ const fold=document.createElement('details');fold.className='execweave-message-history';
468
+ const key='message:'+messageKey(message);fold.dataset.foldKey=key;fold.open=state.get(key)===true;
469
+ const summary=document.createElement('summary');
470
+ summary.textContent=[moment(message.timestamp),`${message.sender||'Unknown sender'} → ${message.recipient||'Recipient not recorded'}`].filter(Boolean).join(' · ');
471
+ const body=document.createElement('pre');body.className='execweave-message-body';body.textContent=displayText(message);
472
+ fold.append(summary,body);fold.addEventListener('toggle',()=>state.set(key,fold.open));section.appendChild(fold);
473
+ }
474
+ return section;
475
+ }
443
476
  function renderNode(node){
444
477
  const rows=nodeCards(node);
445
478
  if(!rows.length)return false;
@@ -460,8 +493,8 @@ function render(node){
460
493
  const isRoot=nodeHasRootAuthority(node)||previewUsesRootRenderer(preview);
461
494
  const rounds=(isRoot?rootRounds(messages,path||'/root'):childRounds(messages,path)).map(round=>execweaveFillAssignedTask(round,node,isRoot));
462
495
  const tools=toolCallsFor(String(node.id||''));
463
- const frameworkCommunication=attrs(node).conversation_scope==='framework_agent'?uniqueTexts(messages.filter(message=>isObserved(message)&&!isInjected(message)&&String(message?.kind||'')==='agent_message')).join('\n\n'):'';
464
- const appendTools=()=>{if(frameworkCommunication)details.appendChild(card('Agent communication',frameworkCommunication));if(tools)details.appendChild(card('Tools',tools))};
496
+ const communication=agentCommunicationHistory(node,messages);
497
+ const appendTools=()=>{if(communication)details.appendChild(communication);if(tools)details.appendChild(card('Tools',tools))};
465
498
  if(rounds.length<2){const fallback={cards:isRoot?[['Prompt',''],['Final response','']]:[['Task',''],['Thinking',''],['Response','']]};details.appendChild(roundView(rounds[0]||execweaveFillAssignedTask(fallback,node,isRoot)));appendTools();return true}
466
499
  // A subagent borrows the moment and the wording of the unique canonical root round
467
500
  // it belongs to. If root identity is ambiguous, the child keeps its own timestamp.
@@ -571,6 +571,7 @@ EXECUTION_FLOW_SCRIPT = r"""
571
571
  const ranks=[...widthByRank.keys()].sort((a,b)=>a-b);
572
572
  const minRank=ranks[0],rootItems=ranked.filter(item=>item.rank===minRank);
573
573
  let x=Math.min(...rootItems.map(item=>item.spec.x));
574
+ if(rootItems.some(item=>attrsOf(nodeById.get(item.id)).viewer_session_flow))x=Math.min(...[...topo.spec.values()].map(spec=>spec.x));
574
575
  if(!Number.isFinite(x))x=0;
575
576
  const xByRank=new Map();
576
577
  for(const rank of ranks){
@@ -102,4 +102,6 @@ def normalize_provider_execution_flow(html: str) -> str:
102
102
  if html.count(needle) != 1:
103
103
  raise RuntimeError(f"execution-flow provider normalization seam changed: {label}")
104
104
  html = html.replace(needle, replacement, 1)
105
- return html
105
+ from .viewer_session_flow import inject_session_flow
106
+
107
+ return inject_session_flow(html)
@@ -123,23 +123,8 @@ _WAIT_AWARE_ACTIVATION = """ const spawnEdges=rawEdges.filter(edge=>
123
123
  if(!ended)occurrence.targets.add(child);
124
124
  }
125
125
  }
126
- // The branch model answers "which model does this target agent belong to?".
127
- // Prefer a unanimous target-agent model context over the actor's current model.
128
- // This keeps old children under gpt-5.5 even if /root later switches to Luna.
129
- for(const occurrence of occurrences){
130
- if(!occurrence.targets.size)continue;
131
- // Child model lookup ignores the actor tool-call/item hint. Only the child's
132
- // own model evidence may win, and every target must share one known model.
133
- const targetModels=[];
134
- let allKnown=true;
135
- for(const target of occurrence.targets){
136
- const modelId=resolveModel(target,null);
137
- if(!modelId){allKnown=false;break}
138
- targetModels.push(modelId);
139
- }
140
- const unique=new Set(targetModels);
141
- if(allKnown&&unique.size===1)occurrence.modelId=[...unique][0];
142
- }
126
+ // Actions belong to the actor's model at the time of the action. A child's
127
+ // model never changes the model that issued a delegation, message, or wait.
143
128
  const resolvedOccurrences=occurrences.filter(occurrence=>occurrence.targets.size>0);
144
129
  if(!resolvedOccurrences.length)return display;
145
130
  const flowKey=occurrence=>`${occurrence.owner}\\u0000${occurrence.kind}\\u0000${occurrence.modelId||''}`;
@@ -284,8 +269,8 @@ def harden_execution_flow_projection(html: str) -> str:
284
269
  """Fail closed on ambiguous or evidence-poor execution-flow projection.
285
270
 
286
271
  A model context is shown only for orchestration that resolves to real target agents.
287
- The target agents' evidenced model context is authoritative when all resolved targets
288
- agree; the actor model is only a fallback. Explicit targets are preferred.
272
+ The actor's evidenced model context owns the action, independently of the target's
273
+ model. Explicit targets are preferred.
289
274
  Target-less ``wait_agent`` calls may use the provider-evidenced active-child set:
290
275
  children spawned before the wait and without stop/close evidence before that wait.
291
276
  Exact requester -> subtask -> assigned-child chains and exact owner-bound tool-call
@@ -361,4 +346,4 @@ def harden_execution_flow_projection(html: str) -> str:
361
346
  if html.count(_FLOW_EARLY_RETURN) != 1 or html.count(_FLOW_END) != 1:
362
347
  raise RuntimeError("execution-flow final geometry seam changed")
363
348
  html = html.replace(_FLOW_EARLY_RETURN, _FLOW_SAFE_EARLY_RETURN, 1)
364
- return html.replace(_FLOW_END, _FLOW_SAFE_END, 1)
349
+ return html.replace(_FLOW_END, _FLOW_SAFE_END, 1)
@@ -91,6 +91,10 @@ function execweaveFlowDisplay(data,display){
91
91
  // another one inherits that node's layer and row, and reading those back places it
92
92
  // exactly on top of the node it was copied from.
93
93
  function execweaveFlowPreference(id){
94
+ const projected=nodeById.get(id)?.attributes||{};
95
+ // The browser's session projection runs after the Python flow projection. Its
96
+ // explicit ranks supersede copied resource layers, including synthetic nodes.
97
+ if(Number.isInteger(projected.viewer_flow_rank)&&[...nodeById.values()].some(n=>n.attributes?.viewer_session_flow))return{layer:projected.viewer_flow_rank,row:0};
94
98
  const owned=execweaveFlowNodeIds();
95
99
  // A folded member is still drawn when the browser's file/type budget keeps it. Its
96
100
  // annotation carries the survivor's solved layer and row, so keep that preference;
@@ -163,6 +167,12 @@ function execweaveFlowSolve(){
163
167
  // stack, which is to say only when it closes a cycle. Every other edge is kept, so a
164
168
  // node always takes a column after the nodes that lead to it.
165
169
  const feedback=new Set(),state=new Map(ids.map(id=>[id,0]));
170
+ // A recorded reply remains a return even if the dispatch was outside this
171
+ // recording; do not move the main agent behind its received message history.
172
+ for(const pair of pairs){
173
+ const source=nodeById.get(pair.source),target=nodeById.get(pair.target);
174
+ if(target?.attributes?.viewer_session_flow&&(source?.attributes?.viewer_framework_messages||source?.attributes?.viewer_return_message))feedback.add(pair.source+'|'+pair.target);
175
+ }
166
176
  // With a viewer_flow payload, prefer the projection's layer order when choosing the
167
177
  // DFS roots. Without one (the dashboard shell also accepts a raw static graph), keep
168
178
  // the graph's insertion order: sorting IDs would visit `agent:child:*` before
@@ -180,6 +190,7 @@ function execweaveFlowSolve(){
180
190
  const peers=after.get(frame.id);
181
191
  if(frame.next>=peers.length){state.set(frame.id,2);stack.pop();continue}
182
192
  const peer=peers[frame.next++];
193
+ if(feedback.has(frame.id+'|'+peer))continue;
183
194
  const seen=state.get(peer);
184
195
  if(seen===1)feedback.add(frame.id+'|'+peer);
185
196
  else if(seen===0){state.set(peer,1);stack.push({id:peer,next:0})}
@@ -0,0 +1,54 @@
1
+ """Framework-only communication projection. Provider graphs pass through unchanged."""
2
+
3
+ FRAMEWORK_FLOW_SCRIPT = r"""
4
+ function execweaveFrameworkMessages(display,raw){
5
+ const a=n=>n?.attributes||{},r=e=>String(e?.relation||'').toUpperCase();
6
+ const agents=new Map((raw?.nodes||[]).filter(n=>n.type==='agent'&&a(n).conversation_scope==='framework_agent').map(n=>[n.id,n]));
7
+ if(!agents.size)return display;
8
+ const groups=new Map();
9
+ for(const e of raw.edges||[]){
10
+ if(!['MESSAGE_SENT','MESSAGE_RECEIVED'].includes(r(e))||!agents.has(e.source)||!agents.has(e.target)||e.source===e.target||e.inferred===true)continue;
11
+ const key=`${e.source}\0${e.target}`;
12
+ if(!groups.has(key))groups.set(key,{source:e.source,target:e.target,edges:[]});
13
+ groups.get(key).edges.push(e);
14
+ }
15
+ if(!groups.size)return display;
16
+ let nodes=display.nodes.map(n=>({...n,attributes:{...a(n)}}));
17
+ const hidden=new Set(),hiddenNodes=new Set(),added=[];
18
+ // Stream-only messages and callback logs remain in their owner's inspector.
19
+ // They are not delivery events and must not create dangling routing branches.
20
+ const providers=new Set([...agents.values()].map(n=>a(n).provider));
21
+ for(const n of raw.nodes||[]){
22
+ if(n.type!=='message'||!providers.has(a(n).provider))continue;
23
+ const anchors=(raw.edges||[]).filter(e=>(e.target===n.id&&agents.has(e.source)&&r(e)==='MESSAGE_SENT')||(e.source===n.id&&r(e)==='OBSERVED_AT_PROCESS'));
24
+ if(!anchors.length)continue;
25
+ for(const e of anchors){
26
+ const owner=nodes.find(x=>x.id===(e.source===n.id?e.target:e.source));
27
+ if(owner)owner.attributes.viewer_message_evidence=[...(a(owner).viewer_message_evidence||[]),{message_id:n.id,routing_status:'recipient_not_recorded',evidence_edge_id:e.id}];
28
+ }
29
+ hiddenNodes.add(n.id);
30
+ }
31
+ nodes=nodes.filter(n=>!hiddenNodes.has(n.id));
32
+ for(const group of groups.values()){
33
+ const id=`viewer:messages:${encodeURIComponent(group.source)}:${encodeURIComponent(group.target)}`;
34
+ const receiver=agents.get(group.target);
35
+ const evidence=group.edges.map(e=>e.id).filter(Boolean);
36
+ for(const eid of evidence)hidden.add(eid);
37
+ nodes.push({id,type:'message',name:`Messages → ${receiver.name||group.target}`,attributes:{
38
+ viewer_only:true,viewer_framework_messages:true,provider:a(receiver).provider,
39
+ sender_agent_id:group.source,recipient_agent_id:group.target,
40
+ viewer_message_observations:group.edges.map(e=>({relation:e.relation,count:e.count||1,first_seen:e.first_seen,last_seen:e.last_seen,evidence_edge_id:e.id})),
41
+ evidence_edge_ids:evidence,
42
+ }});
43
+ const delivery=group.edges.some(e=>r(e)==='MESSAGE_RECEIVED')?'DELIVERED_AGENT_MESSAGE':'ADDRESSED_TO';
44
+ for(const [source,target,relation] of [[group.source,id,'MESSAGE_SENT'],[id,group.target,delivery]]){
45
+ added.push({id:`viewer:${source}--${relation}-->${target}`,source,target,relation,count:1,viewer_only:true,causal:false,inferred:false,
46
+ first_sequence:Math.min(...group.edges.map(e=>e.first_sequence).filter(Number.isInteger)),
47
+ first_seen:group.edges.map(e=>e.first_seen).filter(Boolean).sort()[0],
48
+ attributes:{evidence_edge_ids:evidence,routing_basis:'framework_explicit_participants'}});
49
+ }
50
+ }
51
+ const edges=display.edges.filter(e=>!hidden.has(e.id)&&!hiddenNodes.has(e.source)&&!hiddenNodes.has(e.target)).concat(added);
52
+ return {...display,nodes,edges,node_count:nodes.length,edge_count:edges.length};
53
+ }
54
+ """
@@ -355,7 +355,11 @@ def collapse_framework_tasks(
355
355
  task_id
356
356
  for task_id, _ in sorted(
357
357
  candidates.items(),
358
- key=lambda item: (item[1], item[0]),
358
+ key=lambda item: (
359
+ not bool(_attrs(node_by_id[item[0]]).get("action")),
360
+ bool(_attrs(node_by_id[item[0]]).get("task_prompt") or _attrs(node_by_id[item[0]]).get("prompt")),
361
+ item[1], item[0],
362
+ ),
359
363
  reverse=True,
360
364
  )
361
365
  ]
@@ -0,0 +1,166 @@
1
+ """Session/actor presentation over evidence; never mutates the recorded graph."""
2
+
3
+ SESSION_FLOW_SCRIPT = r"""
4
+ function execweaveSessionFlow(display,raw){
5
+ const a=n=>n?.attributes||{},r=e=>String(e?.relation||'').toUpperCase();
6
+ const rawNodes=raw?.nodes||[],rawEdges=raw?.edges||[];
7
+ const byId=new Map(rawNodes.map(n=>[n.id,n]));
8
+ const sessions=rawNodes.filter(n=>n.type==='session');
9
+ // A recording can contain multiple roots/sessions. Never choose by array order.
10
+ const roots=rawNodes.filter(n=>n.type==='agent'&&(a(n).agent_role==='root'||a(n).viewer_root===true||a(n).agent_path==='/root'||n.name==='/root'));
11
+ if(!sessions.length||!roots.length)return display;
12
+ let nodes=display.nodes.map(n=>({...n,attributes:{...a(n),...(Number.isFinite(a(n).viewer_flow_rank)&&a(n).viewer_flow_rank>0?{viewer_flow_rank:a(n).viewer_flow_rank+1}:{})}}));
13
+ let edges=display.edges.map(e=>({...e}));
14
+ const observed=e=>e&&e.inferred!==true&&e.viewer_only!==true&&a(e).inferred!==true;
15
+ const unique=items=>{const ids=[...new Set(items.filter(Boolean))];return ids.length===1?ids[0]:null};
16
+ const ownerOf=(id,seen=new Set())=>{
17
+ const n=byId.get(id);if(!n||seen.has(id))return null;
18
+ if(n.type==='agent')return id;
19
+ if(!['provider_session','agent_turn','agent_execution','tool_call','tool_call_observation'].includes(n.type))return null;
20
+ seen=new Set(seen);seen.add(id);
21
+ return unique(rawEdges.filter(e=>e.target===id&&observed(e)).map(e=>ownerOf(e.source,seen)));
22
+ };
23
+ const rootSessions=new Map(),unresolved=[];
24
+ for(const root of roots){
25
+ const direct=rawEdges.filter(e=>e.source===root.id&&sessions.some(s=>s.id===e.target)&&observed(e));
26
+ let sid=unique(direct.map(e=>e.target));
27
+ // The run id is explicit recording membership, not a claim that the provider
28
+ // launched a process. Show that membership with a non-causal display edge.
29
+ if(!sid&&roots.length===1&&sessions.length===1&&sessions[0].id===`session:${raw.session_id}`)sid=sessions[0].id;
30
+ if(!sid){unresolved.push(root.id);continue}
31
+ rootSessions.set(root.id,sid);
32
+ const session=byId.get(sid);
33
+ if(!nodes.some(n=>n.id===sid))nodes.push({...session,attributes:{...a(session)}});
34
+ if(!edges.some(e=>e.source===root.id&&e.target===sid))edges.push({
35
+ id:`viewer:${root.id}--RECORDED_IN_SESSION-->${sid}`,source:root.id,target:sid,
36
+ relation:'RECORDED_IN_SESSION',count:1,viewer_only:true,causal:false,inferred:false,
37
+ attributes:{evidence_node_ids:[root.id,sid],recording_session_id:raw.session_id}
38
+ });
39
+ const sessionNode=nodes.find(n=>n.id===sid);sessionNode.attributes.viewer_flow_rank=1;
40
+ sessionNode.attributes.viewer_owner_agent_id=root.id;
41
+ const rootNode=nodes.find(n=>n.id===root.id);if(rootNode){rootNode.attributes.viewer_flow_rank=0;rootNode.attributes.viewer_session_flow=true}
42
+ }
43
+ if(!rootSessions.size)return display;
44
+ const modelRelations=new Set(['USED_MODEL','INVOKED_MODEL','INVOKES_MODEL','REQUESTED_MODEL','REQUESTS_MODEL_CALL','INFERRED','SWITCHED_MODEL','MODEL_INVOCATION_COMPLETED','MODEL_CALL_RESPONDS']);
45
+ const modelUses=[];
46
+ for(const e of rawEdges){
47
+ if(byId.get(e.target)?.type!=='model'||!modelRelations.has(r(e))||!observed(e))continue;
48
+ const owner=ownerOf(e.source);if(owner)modelUses.push({owner,model:e.target,edge:e});
49
+ }
50
+ const contextByKey=new Map();
51
+ for(const n of nodes)if(a(n).viewer_model_context)contextByKey.set(`${a(n).owner_agent_id}\0${a(n).model_resource_id}`,n.id);
52
+ const ensure=(owner,model)=>{
53
+ const key=`${owner}\0${model}`;let id=contextByKey.get(key);
54
+ const sid=rootSessions.get(owner);if(!sid)return id||null;
55
+ if(!id){
56
+ const resource=byId.get(model);if(!resource)return null;
57
+ id=`viewer:model-context:${encodeURIComponent(owner)}:${encodeURIComponent(model)}`;
58
+ nodes.push({...resource,id,attributes:{...a(resource),viewer_only:true,viewer_model_context:true,owner_agent_id:owner,model_resource_id:model,viewer_flow_rank:2}});
59
+ contextByKey.set(key,id);
60
+ }
61
+ const node=nodes.find(n=>n.id===id);if(node)node.attributes.viewer_flow_rank=2;
62
+ edges=edges.filter(e=>!(e.source===owner&&e.target===id&&r(e)==='MODEL_CONTEXT'));
63
+ if(!edges.some(e=>e.source===sid&&e.target===id))edges.push({
64
+ id:`viewer:${sid}--MODEL_CONTEXT-->${id}`,source:sid,target:id,relation:'MODEL_CONTEXT',
65
+ count:1,viewer_only:true,causal:false,inferred:false,
66
+ attributes:{owner_agent_id:owner,model_resource_id:model,evidence_edge_ids:modelUses.filter(u=>u.owner===owner&&u.model===model).map(u=>u.edge.id)}
67
+ });
68
+ return id;
69
+ };
70
+ for(const use of modelUses)if(rootSessions.has(use.owner))ensure(use.owner,use.model);
71
+ for(const n of [...nodes])if(a(n).viewer_model_context&&rootSessions.has(a(n).owner_agent_id))ensure(a(n).owner_agent_id,a(n).model_resource_id);
72
+ // Replace only the same actor/model's invocation edge. Child usage remains
73
+ // independent; a shared model name cannot steal its owner or session.
74
+ edges=edges.filter(e=>{
75
+ if(!modelRelations.has(r(e)))return true;
76
+ const owner=ownerOf(e.source);
77
+ return !(rootSessions.has(owner)&&contextByKey.has(`${owner}\0${e.target}`));
78
+ });
79
+ const modelAt=(owner,item)=>{
80
+ const hint=a(item).model||a(item).model_name||a(item).codex_model;
81
+ const uses=modelUses.filter(u=>u.owner===owner&&!['MODEL_INVOCATION_COMPLETED','MODEL_CALL_RESPONDS'].includes(r(u.edge)));
82
+ if(hint){const matched=unique(uses.filter(u=>u.model===hint||byId.get(u.model)?.name===hint||a(byId.get(u.model)).model_name===hint).map(u=>u.model));if(matched)return matched;return null}
83
+ const seq=item.first_sequence,stamp=item.first_seen;
84
+ const points=uses.flatMap(u=>[u,...(u.edge.last_sequence!==u.edge.first_sequence||u.edge.last_seen!==u.edge.first_seen?[{...u,edge:{...u.edge,first_sequence:u.edge.last_sequence,first_seen:u.edge.last_seen}}]:[])]);
85
+ // An aggregated model edge may have omitted intervening switches. Without
86
+ // an exact model hint, do not pretend its two endpoints are a complete log.
87
+ if(new Set(uses.map(u=>u.model)).size>1&&uses.some(u=>u.edge.count>2&&(Number.isInteger(seq)?u.edge.first_sequence<seq&&seq<u.edge.last_sequence:u.edge.first_seen<stamp&&stamp<u.edge.last_seen)))return null;
88
+ const timed=points.filter(u=>Number.isInteger(seq)&&Number.isInteger(u.edge.first_sequence)?u.edge.first_sequence<=seq:stamp&&u.edge.first_seen&&u.edge.first_seen<=stamp);
89
+ if(!timed.length)return null;
90
+ timed.sort((x,y)=>Number.isInteger(seq)?(y.edge.first_sequence||0)-(x.edge.first_sequence||0):String(y.edge.first_seen).localeCompare(String(x.edge.first_seen)));
91
+ const best=timed[0].edge;
92
+ return unique(timed.filter(u=>Number.isInteger(seq)?u.edge.first_sequence===best.first_sequence:u.edge.first_seen===best.first_seen).map(u=>u.model));
93
+ };
94
+ // Split call edges by exact occurrence ownership and model. A tool resource
95
+ // can be shared, but its calls must never inherit another actor's model.
96
+ const routedEdges=[];
97
+ for(const e of [...edges]){
98
+ if(!rootSessions.has(e.source)||!['CALLED_TOOL','REQUESTED_TOOL_CALL','REQUESTS_TOOL_CALL','USES_TOOL','PERFORMED_ORCHESTRATION','MESSAGE_SENT'].includes(r(e))){routedEdges.push(e);continue}
99
+ const target=nodes.find(n=>n.id===e.target);if(!target){routedEdges.push(e);continue}
100
+ const occurrences=(e.viewer_tool_call_occurrences||a(target).viewer_tool_call_occurrences||[]).filter(o=>!o.owner_id||o.owner_id===e.source);
101
+ const buckets=new Map();
102
+ for(const item of occurrences.length?occurrences:[null]){
103
+ const native=item&&(item.call_ids||[]).map(id=>byId.get(id)).find(Boolean);
104
+ const model=modelAt(e.source,item?{...item,attributes:a(native)}:{...e,attributes:a(target)});
105
+ if(!buckets.has(model))buckets.set(model,[]);
106
+ if(item)buckets.get(model).push(item);
107
+ }
108
+ for(const [model,items] of buckets){
109
+ const context=model&&ensure(e.source,model);
110
+ const source=context||e.source;
111
+ routedEdges.push({...e,id:context?`viewer:${context}--${r(e)}-->${e.target}:${encodeURIComponent(e.id)}`:e.id,source,
112
+ ...(occurrences.length?{count:items.length,evidence_call_count:items.length,viewer_tool_call_occurrences:items}:{}),
113
+ ...(context?{viewer_only:true,causal:false,viewer_original_edge_id:e.id}:{}),
114
+ attributes:{...a(e),...(context?{model_resource_id:model}: {model_attribution:'not_observed'})}});
115
+ }
116
+ }
117
+ edges=routedEdges;
118
+ // A received message goes back to its actual recipient. The sender's model is
119
+ // a separate invocation, never a substitute destination for the reply.
120
+ for(const n of nodes){
121
+ if(!a(n).viewer_orchestration_action||a(n).orchestration_kind!=='send_input')continue;
122
+ const owner=a(n).owner_agent_id;
123
+ const returns=edges.filter(e=>e.source===n.id&&rootSessions.has(e.target)&&e.target!==owner);
124
+ if(!returns.length)continue;
125
+ edges=edges.map(e=>e.target===n.id&&r(e)==='PERFORMED_ORCHESTRATION'?{...e,source:owner,relation:'SENT_AGENT_MESSAGE',viewer_original_source:e.source}:e);
126
+ n.attributes.viewer_return_message=true;
127
+ delete n.attributes.viewer_flow_rank;
128
+ }
129
+ const usedIds=new Set(edges.flatMap(e=>[e.source,e.target]));
130
+ nodes=nodes.filter(n=>n.type!=='model'||usedIds.has(n.id));
131
+ const known=new Set(nodes.map(n=>n.id));
132
+ edges=edges.filter(e=>known.has(e.source)&&known.has(e.target));
133
+ // Root/session are the forward spine. Return edges remain in the graph but
134
+ // cannot push the root behind its children during layout.
135
+ const ranks=new Map([...rootSessions.keys()].map(id=>[id,0]));
136
+ const queue=[...ranks.keys()];
137
+ while(queue.length){
138
+ const source=queue.shift();
139
+ const outgoing=edges.filter(e=>e.source===source&&!rootSessions.has(e.target));
140
+ outgoing.sort((x,y)=>(r(x)==='MODEL_CONTEXT'?-1:0)-(r(y)==='MODEL_CONTEXT'?-1:0));
141
+ for(const e of outgoing){
142
+ if(ranks.has(e.target))continue;
143
+ ranks.set(e.target,ranks.get(source)+1);queue.push(e.target);
144
+ }
145
+ }
146
+ for(const n of nodes){
147
+ if(ranks.has(n.id))n.attributes.viewer_flow_rank=ranks.get(n.id);
148
+ else if(n.type==='agent')n.attributes.viewer_flow_rank=3;
149
+ }
150
+ return {...display,nodes,edges,node_count:nodes.length,edge_count:edges.length,dashboard_projection:{...display.dashboard_projection,session_flow:true,unresolved_root_sessions:unresolved}};
151
+ }
152
+ function execweaveExecutionFlowProjection(display,raw){
153
+ return execweaveSessionFlow(execweaveFrameworkMessages(execweaveExecutionFlowBeforeSession(display,raw),raw),raw);
154
+ }
155
+ """
156
+
157
+
158
+ def inject_session_flow(script: str) -> str:
159
+ from .viewer_framework_flow import FRAMEWORK_FLOW_SCRIPT
160
+
161
+ seam = " function execweaveExecutionFlowProjection(display,raw){"
162
+ anchor = " if(typeof execweaveDashboardGraph==='function'){"
163
+ if script.count(seam) != 1 or script.count(anchor) != 1:
164
+ raise RuntimeError("session flow projection seam changed")
165
+ script = script.replace(seam, " function execweaveExecutionFlowBeforeSession(display,raw){", 1)
166
+ return script.replace(anchor, FRAMEWORK_FLOW_SCRIPT + SESSION_FLOW_SCRIPT + "\n" + anchor, 1)
File without changes
File without changes
File without changes