Wire message_id on all answer chunks, fix DAG structure (#748)

Wire message_id on all answer chunks, fix DAG structure message_id:
- Add message_id to AgentAnswer dataclass and propagate in
  socket_client._parse_chunk
- Wire message_id into answer callbacks and send_final_response
  for all three patterns (react, plan-then-execute, supervisor)
- Supervisor decomposition thought and synthesis answer chunks
  now carry message_id

DAG structure fixes:
- Observation derives from sub-trace Synthesis (not Analysis)
  when a tool produces a sub-trace; tracked via
  last_sub_explain_uri on context
- Subagent sessions derive from parent's Decomposition via
  parent_uri on agent_session_triples
- Findings derive from subagent Conclusions (not Decomposition)
- Synthesis derives from all findings (multiple wasDerivedFrom)
  ensuring single terminal node
- agent_synthesis_triples accepts list of parent URIs
- Explainability chain walker follows from sub-trace terminal
  to find downstream Observation

Emit Analysis before tool execution:
- Add on_action callback to react() in agent_manager.py, called
  after reason() but before tool invocation
- Orchestrator and old service emit Analysis+ToolUse triples via
  on_action so sub-traces appear after their parent in the stream
This commit is contained in:
cybermaggedon 2026-04-01 13:27:41 +01:00 committed by GitHub
parent 153ae9ad30
commit 2bcf375103
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 134 additions and 28 deletions

View file

@ -1095,6 +1095,15 @@ class ExplainabilityClient:
"trace": sub_trace,
})
# Continue from the sub-trace's terminal entity
# (Observation may derive from Synthesis)
terminal = sub_trace.get("synthesis")
if terminal:
self._follow_provenance_chain(
terminal.uri, trace, graph, user, collection,
max_depth=max_depth - 1,
)
elif isinstance(entity, (Conclusion, Synthesis)):
trace["steps"].append(entity)

View file

@ -397,7 +397,8 @@ class SocketClient:
return AgentAnswer(
content=resp.get("content", ""),
end_of_message=resp.get("end_of_message", False),
end_of_dialog=resp.get("end_of_dialog", False)
end_of_dialog=resp.get("end_of_dialog", False),
message_id=resp.get("message_id", ""),
)
elif chunk_type == "action":
return AgentThought(

View file

@ -188,6 +188,7 @@ class AgentAnswer(StreamingChunk):
"""
chunk_type: str = "final-answer"
end_of_dialog: bool = False
message_id: str = ""
@dataclasses.dataclass
class RAGChunk(StreamingChunk):