agent-orchestrator: add explainability provenance for all patterns (#744)

agent-orchestrator: add explainability provenance for all agent
patterns

Extend the provenance/explainability system to provide
human-readable reasoning traces for the orchestrator's three
agent patterns. Previously only ReAct emitted provenance
(session, iteration, conclusion). Now each pattern records its
cognitive steps as typed RDF entities in the knowledge graph,
using composable mixin types (e.g. Finding + Answer).

New provenance chains:
- Supervisor: Question → Decomposition → Finding ×N → Synthesis
- Plan-then-Execute: Question → Plan → StepResult ×N → Synthesis
- ReAct: Question → Analysis ×N → Conclusion (unchanged)

New RDF types: Decomposition, Finding, Plan, StepResult.
New predicates: tg:subagentGoal, tg:planStep.
Reuses existing Synthesis + Answer mixin for final answers.

Provenance library (trustgraph-base):
- Triple builders, URI generators, vocabulary labels for new types
- Client dataclasses with from_triples() dispatch
- fetch_agent_trace() follows branching provenance chains
- API exports updated

Orchestrator (trustgraph-flow):
- PatternBase emit methods for decomposition, finding, plan, step result, and synthesis
- SupervisorPattern emits decomposition during fan-out
- PlanThenExecutePattern emits plan and step results
- Service emits finding triples on subagent completion
- Synthesis provenance replaces generic final triples

CLI (trustgraph-cli):
- invoke_agent -x displays new entity types inline
This commit is contained in:
cybermaggedon 2026-03-31 12:54:51 +01:00 committed by GitHub
parent e65ea217a2
commit 7b734148b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 560 additions and 82 deletions

View file

@ -20,9 +20,19 @@ from trustgraph.provenance import (
agent_thought_uri,
agent_observation_uri,
agent_final_uri,
agent_decomposition_uri,
agent_finding_uri,
agent_plan_uri,
agent_step_result_uri,
agent_synthesis_uri,
agent_session_triples,
agent_iteration_triples,
agent_final_triples,
agent_decomposition_triples,
agent_finding_triples,
agent_plan_triples,
agent_step_result_triples,
agent_synthesis_triples,
set_graph,
GRAPH_RETRIEVAL,
)
@ -359,6 +369,146 @@ class PatternBase:
explain_graph=GRAPH_RETRIEVAL,
))
# ---- Orchestrator provenance helpers ------------------------------------
async def emit_decomposition_triples(
self, flow, session_id, session_uri, goals, user, collection,
respond, streaming,
):
"""Emit provenance for a supervisor decomposition step."""
uri = agent_decomposition_uri(session_id)
triples = set_graph(
agent_decomposition_triples(uri, session_uri, goals),
GRAPH_RETRIEVAL,
)
await flow("explainability").send(Triples(
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
async def emit_finding_triples(
self, flow, session_id, index, goal, answer_text, user, collection,
respond, streaming,
):
"""Emit provenance for a subagent finding."""
uri = agent_finding_uri(session_id, index)
decomposition_uri = agent_decomposition_uri(session_id)
doc_id = f"urn:trustgraph:agent:{session_id}/finding/{index}/doc"
try:
await self.processor.save_answer_content(
doc_id=doc_id, user=user,
content=answer_text,
title=f"Finding: {goal[:60]}",
)
except Exception as e:
logger.warning(f"Failed to save finding to librarian: {e}")
doc_id = None
triples = set_graph(
agent_finding_triples(uri, decomposition_uri, goal, doc_id),
GRAPH_RETRIEVAL,
)
await flow("explainability").send(Triples(
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
async def emit_plan_triples(
self, flow, session_id, session_uri, steps, user, collection,
respond, streaming,
):
"""Emit provenance for a plan creation."""
uri = agent_plan_uri(session_id)
triples = set_graph(
agent_plan_triples(uri, session_uri, steps),
GRAPH_RETRIEVAL,
)
await flow("explainability").send(Triples(
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
async def emit_step_result_triples(
self, flow, session_id, index, goal, answer_text, user, collection,
respond, streaming,
):
"""Emit provenance for a plan step result."""
uri = agent_step_result_uri(session_id, index)
plan_uri = agent_plan_uri(session_id)
doc_id = f"urn:trustgraph:agent:{session_id}/step/{index}/doc"
try:
await self.processor.save_answer_content(
doc_id=doc_id, user=user,
content=answer_text,
title=f"Step result: {goal[:60]}",
)
except Exception as e:
logger.warning(f"Failed to save step result to librarian: {e}")
doc_id = None
triples = set_graph(
agent_step_result_triples(uri, plan_uri, goal, doc_id),
GRAPH_RETRIEVAL,
)
await flow("explainability").send(Triples(
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
async def emit_synthesis_triples(
self, flow, session_id, previous_uri, answer_text, user, collection,
respond, streaming,
):
"""Emit provenance for a synthesis answer."""
uri = agent_synthesis_uri(session_id)
doc_id = f"urn:trustgraph:agent:{session_id}/synthesis/doc"
try:
await self.processor.save_answer_content(
doc_id=doc_id, user=user,
content=answer_text,
title="Synthesis",
)
except Exception as e:
logger.warning(f"Failed to save synthesis to librarian: {e}")
doc_id = None
triples = set_graph(
agent_synthesis_triples(uri, previous_uri, doc_id),
GRAPH_RETRIEVAL,
)
await flow("explainability").send(Triples(
metadata=Metadata(id=uri, user=user, collection=collection),
triples=triples,
))
if streaming:
await respond(AgentResponse(
chunk_type="explain", content="",
explain_id=uri, explain_graph=GRAPH_RETRIEVAL,
))
# ---- Response helpers ---------------------------------------------------
async def prompt_as_answer(self, client, prompt_id, variables,

View file

@ -11,7 +11,7 @@ import uuid
from ... schema import AgentRequest, AgentResponse, AgentStep, PlanStep
from ..react.types import Action
from . pattern_base import PatternBase
@ -126,6 +126,13 @@ class PlanThenExecutePattern(PatternBase):
thought_text = f"Created plan with {len(plan_steps)} steps"
await think(thought_text, is_final=True)
# Emit plan provenance
step_goals = [ps.get("goal", "") for ps in plan_steps]
await self.emit_plan_triples(
flow, session_id, session_uri, step_goals,
request.user, collection, respond, streaming,
)
# Build PlanStep objects
plan_agent_steps = [
PlanStep(
@ -263,16 +270,10 @@ class PlanThenExecutePattern(PatternBase):
result=step_result,
)
# Emit iteration provenance
prov_act = Action(
thought=f"Plan step {pending_idx}: {goal}",
name=tool_name,
arguments=tool_arguments,
observation=step_result,
)
await self.emit_iteration_triples(
flow, session_id, iteration_num, session_uri,
prov_act, request, respond, streaming,
# Emit step result provenance
await self.emit_step_result_triples(
flow, session_id, pending_idx, goal, step_result,
request.user, collection, respond, streaming,
)
# Build execution step for history
@ -340,9 +341,12 @@ class PlanThenExecutePattern(PatternBase):
streaming=streaming,
)
await self.emit_final_triples(
flow, session_id, iteration_num, session_uri,
response_text, request, respond, streaming,
# Emit synthesis provenance (links back to last step result)
from trustgraph.provenance import agent_step_result_uri
last_step_uri = agent_step_result_uri(session_id, len(plan) - 1)
await self.emit_synthesis_triples(
flow, session_id, last_step_uri,
response_text, request.user, collection, respond, streaming,
)
if self.is_subagent(request):

View file

@ -427,6 +427,7 @@ class Processor(AgentService):
correlation_id = request.correlation_id
subagent_goal = getattr(request, 'subagent_goal', '')
parent_session_id = getattr(request, 'parent_session_id', '')
# Extract the answer from the completion step
answer_text = ""
@ -451,13 +452,26 @@ class Processor(AgentService):
)
return
# Emit finding provenance for this subagent
template = self.aggregator.get_original_request(correlation_id)
if template and parent_session_id:
entry = self.aggregator.correlations.get(correlation_id)
finding_index = len(entry["results"]) - 1 if entry else 0
collection = getattr(template, 'collection', 'default')
await self.supervisor_pattern.emit_finding_triples(
flow, parent_session_id, finding_index,
subagent_goal, answer_text,
template.user, collection,
respond, template.streaming,
)
if all_done:
logger.info(
f"All subagents complete for {correlation_id}, "
f"dispatching synthesis"
)
template = self.aggregator.get_original_request(correlation_id)
if template is None:
logger.error(
f"No template for correlation {correlation_id}"

View file

@ -16,7 +16,7 @@ import uuid
from ... schema import AgentRequest, AgentResponse, AgentStep
from ..react.types import Action, Final
from trustgraph.provenance import agent_finding_uri
from . pattern_base import PatternBase
@ -121,15 +121,9 @@ class SupervisorPattern(PatternBase):
correlation_id = str(uuid.uuid4())
# Emit decomposition provenance
decompose_act = Action(
thought=f"Decomposed into {len(goals)} sub-goals",
name="decompose",
arguments={"goals": json.dumps(goals), "correlation_id": correlation_id},
observation=f"Fanning out {len(goals)} subagents",
)
await self.emit_iteration_triples(
flow, session_id, iteration_num, session_uri,
decompose_act, request, respond, streaming,
await self.emit_decomposition_triples(
flow, session_id, session_uri, goals,
request.user, collection, respond, streaming,
)
# Fan out: emit a subagent request for each goal
@ -207,10 +201,15 @@ class SupervisorPattern(PatternBase):
streaming=streaming,
)
await self.emit_final_triples(
flow, session_id, iteration_num, session_uri,
response_text, request, respond, streaming,
# Emit synthesis provenance (links back to last finding)
last_finding_uri = agent_finding_uri(
session_id, len(subagent_results) - 1
)
await self.emit_synthesis_triples(
flow, session_id, last_finding_uri,
response_text, request.user, collection, respond, streaming,
)
await self.send_final_response(
respond, streaming, response_text, already_streamed=streaming,
)