execweave 0.8.29__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.29 → execweave-0.8.32}/PKG-INFO +1 -1
  2. {execweave-0.8.29 → execweave-0.8.32}/pyproject.toml +2 -2
  3. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/__init__.py +1 -1
  4. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_preview_generic.py +9 -2
  5. {execweave-0.8.29 → 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.29 → execweave-0.8.32}/src/execweave/framework_adapters/metagpt.py +15 -0
  8. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_agent_panel.py +40 -5
  9. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_execution_flow.py +1 -0
  10. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_execution_flow_provider_normalization.py +3 -1
  11. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_execution_flow_safety.py +5 -20
  12. {execweave-0.8.29 → 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.29 → execweave-0.8.32}/src/execweave/viewer_semantic_projection.py +154 -0
  15. execweave-0.8.32/src/execweave/viewer_session_flow.py +166 -0
  16. {execweave-0.8.29 → execweave-0.8.32}/.gitignore +0 -0
  17. {execweave-0.8.29 → execweave-0.8.32}/LICENSE +0 -0
  18. {execweave-0.8.29 → execweave-0.8.32}/README.md +0 -0
  19. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/__main__.py +0 -0
  20. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/_conversation_records_core.py +0 -0
  21. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/_dashboard_shell_base.py +0 -0
  22. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/_http_proxy_base.py +0 -0
  23. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/_http_proxy_bounded.py +0 -0
  24. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/_http_proxy_stage.py +0 -0
  25. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/agent_bootstrap.py +0 -0
  26. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/agent_topology.py +0 -0
  27. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/agent_trace.py +0 -0
  28. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/agy_preview_sanitize.py +0 -0
  29. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/analysis.py +0 -0
  30. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/anthropic.py +0 -0
  31. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/anthropic_cli.py +0 -0
  32. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/anthropic_full_fidelity.py +0 -0
  33. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/antigravity_adapter.py +0 -0
  34. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/antigravity_adapter_base.py +0 -0
  35. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/antigravity_full_fidelity.py +0 -0
  36. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/antigravity_full_fidelity_base.py +0 -0
  37. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/antigravity_full_fidelity_collaboration_base.py +0 -0
  38. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/antigravity_hook_cli.py +0 -0
  39. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/antigravity_record.py +0 -0
  40. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/antigravity_subagent_linkage.py +0 -0
  41. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/antigravity_trace_capability.py +0 -0
  42. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/auto_specialized.py +0 -0
  43. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/backends.py +0 -0
  44. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/benchmark.py +0 -0
  45. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/claude_adapter.py +0 -0
  46. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/claude_child_transcript.py +0 -0
  47. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/claude_delegation.py +0 -0
  48. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/claude_full_fidelity.py +0 -0
  49. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/claude_hook_cli.py +0 -0
  50. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/claude_hook_contract.py +0 -0
  51. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/claude_model_observer.py +0 -0
  52. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/claude_record.py +0 -0
  53. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/cli.py +0 -0
  54. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_adapter.py +0 -0
  55. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_conversation.py +0 -0
  56. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_full_fidelity.py +0 -0
  57. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_hook_cli.py +0 -0
  58. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_hook_entry.py +0 -0
  59. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_hook_lifecycle.py +0 -0
  60. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_message_diagnostics.py +0 -0
  61. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_message_transport_diagnostics.py +0 -0
  62. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_record.py +0 -0
  63. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_rollout_structures.py +0 -0
  64. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_rollout_trace.py +0 -0
  65. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/codex_rollout_trace_base.py +0 -0
  66. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/collector.py +0 -0
  67. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/command.py +0 -0
  68. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/content_evidence.py +0 -0
  69. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/content_store.py +0 -0
  70. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_archive.py +0 -0
  71. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_message_identity.py +0 -0
  72. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_preview.py +0 -0
  73. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_preview_antigravity.py +0 -0
  74. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_preview_claude.py +0 -0
  75. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_preview_codex.py +0 -0
  76. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_preview_common.py +0 -0
  77. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_preview_lines.py +0 -0
  78. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_preview_transcript.py +0 -0
  79. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_records.py +0 -0
  80. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_records_antigravity.py +0 -0
  81. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_records_codex.py +0 -0
  82. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_records_common.py +0 -0
  83. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/conversation_records_ollama.py +0 -0
  84. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/correlation.py +0 -0
  85. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/cursor_adapter.py +0 -0
  86. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/cursor_delegation.py +0 -0
  87. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/cursor_delegation_base.py +0 -0
  88. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/cursor_full_fidelity.py +0 -0
  89. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/cursor_hook_cli.py +0 -0
  90. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/cursor_hook_contract.py +0 -0
  91. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/cursor_lifecycle.py +0 -0
  92. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/cursor_record.py +0 -0
  93. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/dashboard_shell.py +0 -0
  94. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/entry.py +0 -0
  95. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/evidence_availability.py +0 -0
  96. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/evidence_grade.py +0 -0
  97. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/fidelity.py +0 -0
  98. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/filesystem.py +0 -0
  99. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/focus.py +0 -0
  100. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/framework_adapters/__init__.py +0 -0
  101. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/framework_adapters/autogen.py +0 -0
  102. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/framework_adapters/base.py +0 -0
  103. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/framework_adapters/registry.py +0 -0
  104. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/graph.py +0 -0
  105. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/graph_ops.py +0 -0
  106. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/http_proxy.py +0 -0
  107. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/http_proxy_cli.py +0 -0
  108. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/inference_gateway.py +0 -0
  109. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/inference_gateway_cli.py +0 -0
  110. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/inference_gateway_full_fidelity.py +0 -0
  111. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/inference_identity.py +0 -0
  112. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/inference_identity_cli.py +0 -0
  113. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/integrity.py +0 -0
  114. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/integrity_cli.py +0 -0
  115. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/litellm_callback.py +0 -0
  116. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/litellm_callback_cli.py +0 -0
  117. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live.py +0 -0
  118. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_core.py +0 -0
  119. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view.py +0 -0
  120. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view_extra_style.py +0 -0
  121. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view_markup.py +0 -0
  122. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view_process_layout.py +0 -0
  123. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view_readability.py +0 -0
  124. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view_script_a.py +0 -0
  125. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view_script_b.py +0 -0
  126. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view_script_c.py +0 -0
  127. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view_script_d.py +0 -0
  128. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/live_view_style.py +0 -0
  129. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/model_runtime.py +0 -0
  130. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/model_runtime_cli.py +0 -0
  131. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/model_runtime_full_fidelity.py +0 -0
  132. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/openai_compatible.py +0 -0
  133. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/openai_compatible_cli.py +0 -0
  134. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/openai_compatible_full_fidelity.py +0 -0
  135. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/opencode_adapter.py +0 -0
  136. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/opencode_event_contract.py +0 -0
  137. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/opencode_full_fidelity.py +0 -0
  138. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/opencode_hook_cli.py +0 -0
  139. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/opencode_plugin_cli.py +0 -0
  140. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/opencode_record.py +0 -0
  141. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/opencode_task_linkage.py +0 -0
  142. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/overhead_benchmark.py +0 -0
  143. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/provider_capability.py +0 -0
  144. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/provider_lifecycle.py +0 -0
  145. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/provider_record.py +0 -0
  146. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/rule_pack.py +0 -0
  147. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/rule_pack_cli.py +0 -0
  148. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/runtime_endpoint_availability.py +0 -0
  149. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/scalability_benchmark.py +0 -0
  150. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/schema.py +0 -0
  151. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/scope.py +0 -0
  152. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/semantic.py +0 -0
  153. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/sink.py +0 -0
  154. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/strace_backend.py +0 -0
  155. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/stream_assembly.py +0 -0
  156. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/theme.py +0 -0
  157. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/top.py +0 -0
  158. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/top_cli.py +0 -0
  159. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/validate.py +0 -0
  160. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/vendor/dagre.min.js +0 -0
  161. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/view_cli.py +0 -0
  162. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer.py +0 -0
  163. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_agent_panel_antigravity.py +0 -0
  164. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_agent_panel_claude.py +0 -0
  165. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_agent_panel_codex.py +0 -0
  166. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_agent_panel_cursor.py +0 -0
  167. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_agent_panel_default.py +0 -0
  168. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_agent_panel_ollama.py +0 -0
  169. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_agent_panel_opencode.py +0 -0
  170. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_agent_panel_registry.py +0 -0
  171. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_content_inspector.py +0 -0
  172. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_dashboard_clean.py +0 -0
  173. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_dashboard_focus.py +0 -0
  174. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_dashboard_hardening.py +0 -0
  175. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_dashboard_hardening_v2.py +0 -0
  176. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_dashboard_pr70.py +0 -0
  177. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_edge_decross.py +0 -0
  178. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_external_endpoints.py +0 -0
  179. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_flow_layout.py +0 -0
  180. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_gif.py +0 -0
  181. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_layout_geometry.py +0 -0
  182. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_layout_v2.py +0 -0
  183. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_layout_v2_detached.py +0 -0
  184. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_limits.py +0 -0
  185. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_live_layout.py +0 -0
  186. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_projection.py +0 -0
  187. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_projection_base.py +0 -0
  188. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_projection_bridges.py +0 -0
  189. {execweave-0.8.29 → execweave-0.8.32}/src/execweave/viewer_tool_mirror_identity.py +0 -0
  190. {execweave-0.8.29 → 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.29
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.29 release metadata
8
- version = "0.8.29"
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.29"
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 '));
@@ -420,6 +423,8 @@ function toolOccurrenceSection(node){
420
423
  }
421
424
  function execweaveAssignedTaskText(agent){
422
425
  if(!agent||String(agent.type||'')!=='agent')return '';
426
+ const projected=attrs(agent),attached=projected.viewer_assigned_task_prompt||projected.viewer_assigned_task_name;
427
+ if(attached)return commandText(attached);
423
428
  const graph=rawGraph(),nodes=Array.isArray(graph.nodes)?graph.nodes:[],edges=Array.isArray(graph.edges)?graph.edges:[];
424
429
  const nodeMap=new Map(nodes.filter(node=>node&&node.id).map(node=>[String(node.id),node]));
425
430
  const exactAssignment=['ASSIGNED','AGENT','TASK'].join('_');
@@ -436,8 +441,38 @@ function execweaveFillAssignedTask(round,node,isRoot){
436
441
  const value=round||{cards:[]},cards=Array.isArray(value.cards)?value.cards.map(card=>[card[0],card[1]]):[];
437
442
  const label=isRoot?'Prompt':'Task',index=cards.findIndex(card=>String(card[0]||'')===label);
438
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]);
439
445
  return{...value,cards};
440
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
+ }
441
476
  function renderNode(node){
442
477
  const rows=nodeCards(node);
443
478
  if(!rows.length)return false;
@@ -458,8 +493,8 @@ function render(node){
458
493
  const isRoot=nodeHasRootAuthority(node)||previewUsesRootRenderer(preview);
459
494
  const rounds=(isRoot?rootRounds(messages,path||'/root'):childRounds(messages,path)).map(round=>execweaveFillAssignedTask(round,node,isRoot));
460
495
  const tools=toolCallsFor(String(node.id||''));
461
- const frameworkCommunication=attrs(node).conversation_scope==='framework_agent'?uniqueTexts(messages.filter(message=>isObserved(message)&&!isInjected(message)&&String(message?.kind||'')==='agent_message')).join('\n\n'):'';
462
- 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))};
463
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}
464
499
  // A subagent borrows the moment and the wording of the unique canonical root round
465
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
+ """
@@ -258,6 +258,155 @@ def orphan_audit(nodes: list[dict[str, Any]], edges: list[dict[str, Any]]) -> di
258
258
  return {"count": sum(item["count"] for item in by_type), "by_type": by_type}
259
259
 
260
260
 
261
+ _FRAMEWORK_TASK_RELATIONS = frozenset(
262
+ {"ASSIGNED_TO", "TASK_CREATED", "TASK_STARTED", "TASK_UPDATED", "TASK_COMPLETED", "TASK_FAILED"}
263
+ )
264
+
265
+
266
+ def _framework_task_edge_score(edge: dict[str, Any]) -> tuple[int, str, str]:
267
+ sequence = edge.get("last_sequence")
268
+ if not isinstance(sequence, int) or isinstance(sequence, bool):
269
+ sequence = edge.get("first_sequence")
270
+ if not isinstance(sequence, int) or isinstance(sequence, bool):
271
+ sequence = -1
272
+ return (sequence, str(edge.get("last_seen") or edge.get("first_seen") or ""), str(edge.get("id") or ""))
273
+
274
+
275
+ def _framework_task_text(task: dict[str, Any]) -> str:
276
+ attributes = _attrs(task)
277
+ for value in (
278
+ attributes.get("task_prompt"),
279
+ attributes.get("prompt"),
280
+ attributes.get("description"),
281
+ task.get("name"),
282
+ ):
283
+ if isinstance(value, str) and value.strip():
284
+ return value.strip()
285
+ return ""
286
+
287
+
288
+ def collapse_framework_tasks(
289
+ nodes: list[dict[str, Any]],
290
+ edges: list[dict[str, Any]],
291
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, int]]:
292
+ """Fold framework task presentation into the owning agent, like provider views.
293
+
294
+ Framework adapters retain first-class task entities in the raw graph because task
295
+ lifecycle and prompt evidence must remain auditable. Provider dashboards expose
296
+ that same information on the agent card, however, so the viewer projection hides
297
+ only framework task nodes and copies the latest authoritative task text onto each
298
+ assigned framework agent. Provider graphs are untouched because they do not use
299
+ the framework-agent conversation scope.
300
+ """
301
+ node_by_id = {
302
+ str(node["id"]): node
303
+ for node in nodes
304
+ if isinstance(node, dict) and isinstance(node.get("id"), str)
305
+ }
306
+ framework_agents = {
307
+ node_id
308
+ for node_id, node in node_by_id.items()
309
+ if node.get("type") == "agent" and _attrs(node).get("conversation_scope") == "framework_agent"
310
+ }
311
+ if not framework_agents:
312
+ return nodes, edges, {
313
+ "framework_task_node_count": 0,
314
+ "framework_task_content_node_count": 0,
315
+ "framework_task_agent_count": 0,
316
+ }
317
+
318
+ task_ids: set[str] = set()
319
+ tasks_by_agent: dict[str, list[tuple[tuple[int, str, str], str]]] = defaultdict(list)
320
+ for edge in edges:
321
+ if not isinstance(edge, dict) or edge.get("relation") not in _FRAMEWORK_TASK_RELATIONS:
322
+ continue
323
+ source = str(edge.get("source") or "")
324
+ target = str(edge.get("target") or "")
325
+ source_node = node_by_id.get(source)
326
+ target_node = node_by_id.get(target)
327
+ task_id = source if source_node and source_node.get("type") == "task" else target if target_node and target_node.get("type") == "task" else None
328
+ if task_id is None:
329
+ continue
330
+ agent_id = source if source in framework_agents else target if target in framework_agents else None
331
+ if agent_id is None:
332
+ continue
333
+ task_ids.add(task_id)
334
+ tasks_by_agent[agent_id].append((_framework_task_edge_score(edge), task_id))
335
+
336
+ if not task_ids:
337
+ return nodes, edges, {
338
+ "framework_task_node_count": 0,
339
+ "framework_task_content_node_count": 0,
340
+ "framework_task_agent_count": 0,
341
+ }
342
+
343
+ projected_nodes: list[dict[str, Any]] = []
344
+ for node in nodes:
345
+ node_id = str(node.get("id") or "")
346
+ if node_id in task_ids:
347
+ continue
348
+ if node.get("type") == "agent" and node_id in framework_agents:
349
+ candidates: dict[str, tuple[int, str, str]] = {}
350
+ for score, task_id in tasks_by_agent.get(node_id, []):
351
+ previous = candidates.get(task_id)
352
+ if previous is None or score > previous:
353
+ candidates[task_id] = score
354
+ ordered_ids = [
355
+ task_id
356
+ for task_id, _ in sorted(
357
+ candidates.items(),
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
+ ),
363
+ reverse=True,
364
+ )
365
+ ]
366
+ latest_task = node_by_id.get(ordered_ids[0]) if ordered_ids else None
367
+ attributes = dict(_attrs(node))
368
+ attributes["viewer_assigned_task_ids"] = ordered_ids
369
+ attributes["viewer_assigned_task_count"] = len(ordered_ids)
370
+ if latest_task is not None:
371
+ attributes["viewer_assigned_task_id"] = str(latest_task.get("id") or "")
372
+ attributes["viewer_assigned_task_name"] = str(latest_task.get("name") or "")
373
+ task_text = _framework_task_text(latest_task)
374
+ if task_text:
375
+ attributes["viewer_assigned_task_prompt"] = task_text
376
+ projected_nodes.append({**node, "attributes": attributes})
377
+ continue
378
+ projected_nodes.append(node)
379
+
380
+ hidden_content_ids: set[str] = set()
381
+ for node in nodes:
382
+ node_id = str(node.get("id") or "")
383
+ if node.get("type") != "observed_content" or not node_id:
384
+ continue
385
+ incident = [
386
+ edge
387
+ for edge in edges
388
+ if edge.get("source") == node_id or edge.get("target") == node_id
389
+ ]
390
+ if incident and all(
391
+ (edge.get("source") in task_ids or edge.get("target") in task_ids)
392
+ for edge in incident
393
+ ):
394
+ hidden_content_ids.add(node_id)
395
+
396
+ hidden_ids = task_ids | hidden_content_ids
397
+ projected_nodes = [node for node in projected_nodes if str(node.get("id") or "") not in hidden_content_ids]
398
+ projected_edges = [
399
+ edge
400
+ for edge in edges
401
+ if edge.get("source") not in hidden_ids and edge.get("target") not in hidden_ids
402
+ ]
403
+ return projected_nodes, projected_edges, {
404
+ "framework_task_node_count": len(task_ids),
405
+ "framework_task_content_node_count": len(hidden_content_ids),
406
+ "framework_task_agent_count": len(tasks_by_agent),
407
+ }
408
+
409
+
261
410
  def project_provider_neutral_viewer_graph(graph: dict[str, Any]) -> dict[str, Any]:
262
411
  """Provider-neutral display projection; raw graph/evidence remains unchanged."""
263
412
  from . import viewer_projection_base as base
@@ -268,6 +417,7 @@ def project_provider_neutral_viewer_graph(graph: dict[str, Any]) -> dict[str, An
268
417
  nodes = [deepcopy(n) for n in filtered.get("nodes", []) if isinstance(n, dict)]
269
418
  edges = [deepcopy(e) for e in filtered.get("edges", []) if isinstance(e, dict)]
270
419
  entries = _entries(graph)
420
+ nodes, edges, framework_tasks = collapse_framework_tasks(nodes, edges)
271
421
  nodes, edges, inference = collapse_inference_requests(nodes, edges, entries)
272
422
  roots, _ = _roots(nodes, entries)
273
423
  nodes, edges, files = collapse_orphan_files(nodes, edges, roots[0] if len(roots) == 1 else None)
@@ -297,6 +447,7 @@ def project_provider_neutral_viewer_graph(graph: dict[str, Any]) -> dict[str, An
297
447
  or inference["collapsed_request_count"]
298
448
  or local is not None
299
449
  or files is not None
450
+ or framework_tasks["framework_task_node_count"]
300
451
  )
301
452
  if not topology_changed:
302
453
  if audit["count"]:
@@ -313,6 +464,9 @@ def project_provider_neutral_viewer_graph(graph: dict[str, Any]) -> dict[str, An
313
464
  "orphan_file_node_count": len(files.get("nodes") or []) if files else 0,
314
465
  "inference_request_count": inference["collapsed_request_count"], "logical_inference_count": inference["logical_inference_count"],
315
466
  "direct_inference_edge_count": inference["direct_inference_edge_count"], "unresolved_inference_requests": inference["unresolved"],
467
+ "framework_task_node_count": framework_tasks["framework_task_node_count"],
468
+ "framework_task_content_node_count": framework_tasks["framework_task_content_node_count"],
469
+ "framework_task_agent_count": framework_tasks["framework_task_agent_count"],
316
470
  "internal_hook_node_count": len(hook_nodes), "internal_hook_edge_count": len(hook_edges),
317
471
  "projection_topology_changed": True,
318
472
  })