Update tests for agent-orchestrator (#745)
Add 96 tests covering the orchestrator's aggregation, provenance,
routing, and explainability parsing. These verify the supervisor
fan-out/fan-in lifecycle, the new RDF provenance types
(Decomposition, Finding, Plan, StepResult, Synthesis), and their
round-trip through the wire format.
Unit tests (84):
- Aggregator: register, record completion, peek, build synthesis,
cleanup
- Provenance triple builders: types, provenance links,
goals/steps, labels
- Explainability parsing: from_triples dispatch, field extraction
for all new entity types, precedence over existing types
- PatternBase: is_subagent detection, emit_subagent_completion
message shape
- Completion dispatch: detection logic, full aggregator
integration flow, synthesis request not re-intercepted as
completion
- MetaRouter: task type identification, pattern selection,
valid_patterns constraints, fallback on LLM error or unknown
response
Contract tests (12):
- Orchestration fields on AgentRequest round-trip correctly
- subagent-completion and synthesise step types in request
history
- Plan steps with status and dependencies
- Provenance triple builder → wire format → from_triples
round-trip for all five new entity types
2026-03-31 13:12:26 +01:00
|
|
|
"""
|
|
|
|
|
Unit tests for orchestrator provenance triple builders.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from trustgraph.provenance import (
|
|
|
|
|
agent_decomposition_triples,
|
|
|
|
|
agent_finding_triples,
|
|
|
|
|
agent_plan_triples,
|
|
|
|
|
agent_step_result_triples,
|
|
|
|
|
agent_synthesis_triples,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
from trustgraph.provenance.namespaces import (
|
|
|
|
|
RDF_TYPE, RDFS_LABEL,
|
Split Analysis into Analysis+ToolUse and Observation, add message_id (#747)
Refactor agent provenance so that the decision (thought + tool
selection) and the result (observation) are separate DAG entities:
Question ← Analysis+ToolUse ← Observation ← ... ← Conclusion
Analysis gains tg:ToolUse as a mixin RDF type and is emitted
before tool execution via an on_action callback in react().
This ensures sub-traces (e.g. GraphRAG) appear after their
parent Analysis in the streaming event order.
Observation becomes a standalone prov:Entity with tg:Observation
type, emitted after tool execution. The linear DAG chain runs
through Observation — subsequent iterations and the Conclusion
derive from it, not from the Analysis.
message_id is populated on streaming AgentResponse for thought
and observation chunks, using the provenance URI of the entity
being built. This lets clients group streamed chunks by entity.
Wire changes:
- provenance/agent.py: Add ToolUse type, new
agent_observation_triples(), remove observation from iteration
- agent_manager.py: Add on_action callback between reason() and
tool execution
- orchestrator/pattern_base.py: Split emit, wire message_id,
chain through observation URIs
- orchestrator/react_pattern.py: Emit Analysis via on_action
before tool runs
- agent/react/service.py: Same for non-orchestrator path
- api/explainability.py: New Observation class, updated dispatch
and chain walker
- api/types.py: Add message_id to AgentThought/AgentObservation
- cli: Render Observation separately, [analysis: tool] labels
2026-03-31 17:51:22 +01:00
|
|
|
PROV_ENTITY, PROV_WAS_DERIVED_FROM,
|
Update tests for agent-orchestrator (#745)
Add 96 tests covering the orchestrator's aggregation, provenance,
routing, and explainability parsing. These verify the supervisor
fan-out/fan-in lifecycle, the new RDF provenance types
(Decomposition, Finding, Plan, StepResult, Synthesis), and their
round-trip through the wire format.
Unit tests (84):
- Aggregator: register, record completion, peek, build synthesis,
cleanup
- Provenance triple builders: types, provenance links,
goals/steps, labels
- Explainability parsing: from_triples dispatch, field extraction
for all new entity types, precedence over existing types
- PatternBase: is_subagent detection, emit_subagent_completion
message shape
- Completion dispatch: detection logic, full aggregator
integration flow, synthesis request not re-intercepted as
completion
- MetaRouter: task type identification, pattern selection,
valid_patterns constraints, fallback on LLM error or unknown
response
Contract tests (12):
- Orchestration fields on AgentRequest round-trip correctly
- subagent-completion and synthesise step types in request
history
- Plan steps with status and dependencies
- Provenance triple builder → wire format → from_triples
round-trip for all five new entity types
2026-03-31 13:12:26 +01:00
|
|
|
TG_DECOMPOSITION, TG_FINDING, TG_PLAN_TYPE, TG_STEP_RESULT,
|
|
|
|
|
TG_SYNTHESIS, TG_ANSWER_TYPE, TG_DOCUMENT,
|
|
|
|
|
TG_SUBAGENT_GOAL, TG_PLAN_STEP,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _triple_set(triples):
|
|
|
|
|
"""Convert triples to a set of (s_iri, p_iri, o_value) for easy assertion."""
|
|
|
|
|
result = set()
|
|
|
|
|
for t in triples:
|
|
|
|
|
s = t.s.iri
|
|
|
|
|
p = t.p.iri
|
|
|
|
|
o = t.o.iri if t.o.iri else t.o.value
|
|
|
|
|
result.add((s, p, o))
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _has_type(triples, uri, rdf_type):
|
|
|
|
|
"""Check if a URI has a given rdf:type in the triples."""
|
|
|
|
|
return (uri, RDF_TYPE, rdf_type) in _triple_set(triples)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_values(triples, uri, predicate):
|
|
|
|
|
"""Get all object values for a given subject + predicate."""
|
|
|
|
|
ts = _triple_set(triples)
|
|
|
|
|
return [o for s, p, o in ts if s == uri and p == predicate]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestDecompositionTriples:
|
|
|
|
|
|
|
|
|
|
def test_has_correct_types(self):
|
|
|
|
|
triples = agent_decomposition_triples(
|
|
|
|
|
"urn:decompose", "urn:session", ["goal-a", "goal-b"],
|
|
|
|
|
)
|
|
|
|
|
assert _has_type(triples, "urn:decompose", PROV_ENTITY)
|
|
|
|
|
assert _has_type(triples, "urn:decompose", TG_DECOMPOSITION)
|
|
|
|
|
|
|
|
|
|
def test_not_answer_type(self):
|
|
|
|
|
triples = agent_decomposition_triples(
|
|
|
|
|
"urn:decompose", "urn:session", ["goal-a"],
|
|
|
|
|
)
|
|
|
|
|
assert not _has_type(triples, "urn:decompose", TG_ANSWER_TYPE)
|
|
|
|
|
|
|
|
|
|
def test_links_to_session(self):
|
|
|
|
|
triples = agent_decomposition_triples(
|
|
|
|
|
"urn:decompose", "urn:session", ["goal-a"],
|
|
|
|
|
)
|
|
|
|
|
ts = _triple_set(triples)
|
Split Analysis into Analysis+ToolUse and Observation, add message_id (#747)
Refactor agent provenance so that the decision (thought + tool
selection) and the result (observation) are separate DAG entities:
Question ← Analysis+ToolUse ← Observation ← ... ← Conclusion
Analysis gains tg:ToolUse as a mixin RDF type and is emitted
before tool execution via an on_action callback in react().
This ensures sub-traces (e.g. GraphRAG) appear after their
parent Analysis in the streaming event order.
Observation becomes a standalone prov:Entity with tg:Observation
type, emitted after tool execution. The linear DAG chain runs
through Observation — subsequent iterations and the Conclusion
derive from it, not from the Analysis.
message_id is populated on streaming AgentResponse for thought
and observation chunks, using the provenance URI of the entity
being built. This lets clients group streamed chunks by entity.
Wire changes:
- provenance/agent.py: Add ToolUse type, new
agent_observation_triples(), remove observation from iteration
- agent_manager.py: Add on_action callback between reason() and
tool execution
- orchestrator/pattern_base.py: Split emit, wire message_id,
chain through observation URIs
- orchestrator/react_pattern.py: Emit Analysis via on_action
before tool runs
- agent/react/service.py: Same for non-orchestrator path
- api/explainability.py: New Observation class, updated dispatch
and chain walker
- api/types.py: Add message_id to AgentThought/AgentObservation
- cli: Render Observation separately, [analysis: tool] labels
2026-03-31 17:51:22 +01:00
|
|
|
assert ("urn:decompose", PROV_WAS_DERIVED_FROM, "urn:session") in ts
|
Update tests for agent-orchestrator (#745)
Add 96 tests covering the orchestrator's aggregation, provenance,
routing, and explainability parsing. These verify the supervisor
fan-out/fan-in lifecycle, the new RDF provenance types
(Decomposition, Finding, Plan, StepResult, Synthesis), and their
round-trip through the wire format.
Unit tests (84):
- Aggregator: register, record completion, peek, build synthesis,
cleanup
- Provenance triple builders: types, provenance links,
goals/steps, labels
- Explainability parsing: from_triples dispatch, field extraction
for all new entity types, precedence over existing types
- PatternBase: is_subagent detection, emit_subagent_completion
message shape
- Completion dispatch: detection logic, full aggregator
integration flow, synthesis request not re-intercepted as
completion
- MetaRouter: task type identification, pattern selection,
valid_patterns constraints, fallback on LLM error or unknown
response
Contract tests (12):
- Orchestration fields on AgentRequest round-trip correctly
- subagent-completion and synthesise step types in request
history
- Plan steps with status and dependencies
- Provenance triple builder → wire format → from_triples
round-trip for all five new entity types
2026-03-31 13:12:26 +01:00
|
|
|
|
|
|
|
|
def test_includes_goals(self):
|
|
|
|
|
goals = ["What is X?", "What is Y?", "What is Z?"]
|
|
|
|
|
triples = agent_decomposition_triples(
|
|
|
|
|
"urn:decompose", "urn:session", goals,
|
|
|
|
|
)
|
|
|
|
|
values = _get_values(triples, "urn:decompose", TG_SUBAGENT_GOAL)
|
|
|
|
|
assert set(values) == set(goals)
|
|
|
|
|
|
|
|
|
|
def test_label_includes_count(self):
|
|
|
|
|
triples = agent_decomposition_triples(
|
|
|
|
|
"urn:decompose", "urn:session", ["a", "b", "c"],
|
|
|
|
|
)
|
|
|
|
|
labels = _get_values(triples, "urn:decompose", RDFS_LABEL)
|
|
|
|
|
assert any("3" in label for label in labels)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFindingTriples:
|
|
|
|
|
|
|
|
|
|
def test_has_correct_types(self):
|
|
|
|
|
triples = agent_finding_triples(
|
|
|
|
|
"urn:finding", "urn:decompose", "What is X?",
|
|
|
|
|
)
|
|
|
|
|
assert _has_type(triples, "urn:finding", PROV_ENTITY)
|
|
|
|
|
assert _has_type(triples, "urn:finding", TG_FINDING)
|
|
|
|
|
assert _has_type(triples, "urn:finding", TG_ANSWER_TYPE)
|
|
|
|
|
|
|
|
|
|
def test_links_to_decomposition(self):
|
|
|
|
|
triples = agent_finding_triples(
|
|
|
|
|
"urn:finding", "urn:decompose", "What is X?",
|
|
|
|
|
)
|
|
|
|
|
ts = _triple_set(triples)
|
|
|
|
|
assert ("urn:finding", PROV_WAS_DERIVED_FROM, "urn:decompose") in ts
|
|
|
|
|
|
|
|
|
|
def test_includes_goal(self):
|
|
|
|
|
triples = agent_finding_triples(
|
|
|
|
|
"urn:finding", "urn:decompose", "What is X?",
|
|
|
|
|
)
|
|
|
|
|
values = _get_values(triples, "urn:finding", TG_SUBAGENT_GOAL)
|
|
|
|
|
assert "What is X?" in values
|
|
|
|
|
|
|
|
|
|
def test_includes_document_when_provided(self):
|
|
|
|
|
triples = agent_finding_triples(
|
|
|
|
|
"urn:finding", "urn:decompose", "goal",
|
|
|
|
|
document_id="urn:doc/1",
|
|
|
|
|
)
|
|
|
|
|
values = _get_values(triples, "urn:finding", TG_DOCUMENT)
|
|
|
|
|
assert "urn:doc/1" in values
|
|
|
|
|
|
|
|
|
|
def test_no_document_when_none(self):
|
|
|
|
|
triples = agent_finding_triples(
|
|
|
|
|
"urn:finding", "urn:decompose", "goal",
|
|
|
|
|
)
|
|
|
|
|
values = _get_values(triples, "urn:finding", TG_DOCUMENT)
|
|
|
|
|
assert values == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestPlanTriples:
|
|
|
|
|
|
|
|
|
|
def test_has_correct_types(self):
|
|
|
|
|
triples = agent_plan_triples(
|
|
|
|
|
"urn:plan", "urn:session", ["step-a"],
|
|
|
|
|
)
|
|
|
|
|
assert _has_type(triples, "urn:plan", PROV_ENTITY)
|
|
|
|
|
assert _has_type(triples, "urn:plan", TG_PLAN_TYPE)
|
|
|
|
|
|
|
|
|
|
def test_not_answer_type(self):
|
|
|
|
|
triples = agent_plan_triples(
|
|
|
|
|
"urn:plan", "urn:session", ["step-a"],
|
|
|
|
|
)
|
|
|
|
|
assert not _has_type(triples, "urn:plan", TG_ANSWER_TYPE)
|
|
|
|
|
|
|
|
|
|
def test_links_to_session(self):
|
|
|
|
|
triples = agent_plan_triples(
|
|
|
|
|
"urn:plan", "urn:session", ["step-a"],
|
|
|
|
|
)
|
|
|
|
|
ts = _triple_set(triples)
|
Split Analysis into Analysis+ToolUse and Observation, add message_id (#747)
Refactor agent provenance so that the decision (thought + tool
selection) and the result (observation) are separate DAG entities:
Question ← Analysis+ToolUse ← Observation ← ... ← Conclusion
Analysis gains tg:ToolUse as a mixin RDF type and is emitted
before tool execution via an on_action callback in react().
This ensures sub-traces (e.g. GraphRAG) appear after their
parent Analysis in the streaming event order.
Observation becomes a standalone prov:Entity with tg:Observation
type, emitted after tool execution. The linear DAG chain runs
through Observation — subsequent iterations and the Conclusion
derive from it, not from the Analysis.
message_id is populated on streaming AgentResponse for thought
and observation chunks, using the provenance URI of the entity
being built. This lets clients group streamed chunks by entity.
Wire changes:
- provenance/agent.py: Add ToolUse type, new
agent_observation_triples(), remove observation from iteration
- agent_manager.py: Add on_action callback between reason() and
tool execution
- orchestrator/pattern_base.py: Split emit, wire message_id,
chain through observation URIs
- orchestrator/react_pattern.py: Emit Analysis via on_action
before tool runs
- agent/react/service.py: Same for non-orchestrator path
- api/explainability.py: New Observation class, updated dispatch
and chain walker
- api/types.py: Add message_id to AgentThought/AgentObservation
- cli: Render Observation separately, [analysis: tool] labels
2026-03-31 17:51:22 +01:00
|
|
|
assert ("urn:plan", PROV_WAS_DERIVED_FROM, "urn:session") in ts
|
Update tests for agent-orchestrator (#745)
Add 96 tests covering the orchestrator's aggregation, provenance,
routing, and explainability parsing. These verify the supervisor
fan-out/fan-in lifecycle, the new RDF provenance types
(Decomposition, Finding, Plan, StepResult, Synthesis), and their
round-trip through the wire format.
Unit tests (84):
- Aggregator: register, record completion, peek, build synthesis,
cleanup
- Provenance triple builders: types, provenance links,
goals/steps, labels
- Explainability parsing: from_triples dispatch, field extraction
for all new entity types, precedence over existing types
- PatternBase: is_subagent detection, emit_subagent_completion
message shape
- Completion dispatch: detection logic, full aggregator
integration flow, synthesis request not re-intercepted as
completion
- MetaRouter: task type identification, pattern selection,
valid_patterns constraints, fallback on LLM error or unknown
response
Contract tests (12):
- Orchestration fields on AgentRequest round-trip correctly
- subagent-completion and synthesise step types in request
history
- Plan steps with status and dependencies
- Provenance triple builder → wire format → from_triples
round-trip for all five new entity types
2026-03-31 13:12:26 +01:00
|
|
|
|
|
|
|
|
def test_includes_steps(self):
|
|
|
|
|
steps = ["Define X", "Research Y", "Analyse Z"]
|
|
|
|
|
triples = agent_plan_triples(
|
|
|
|
|
"urn:plan", "urn:session", steps,
|
|
|
|
|
)
|
|
|
|
|
values = _get_values(triples, "urn:plan", TG_PLAN_STEP)
|
|
|
|
|
assert set(values) == set(steps)
|
|
|
|
|
|
|
|
|
|
def test_label_includes_count(self):
|
|
|
|
|
triples = agent_plan_triples(
|
|
|
|
|
"urn:plan", "urn:session", ["a", "b"],
|
|
|
|
|
)
|
|
|
|
|
labels = _get_values(triples, "urn:plan", RDFS_LABEL)
|
|
|
|
|
assert any("2" in label for label in labels)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestStepResultTriples:
|
|
|
|
|
|
|
|
|
|
def test_has_correct_types(self):
|
|
|
|
|
triples = agent_step_result_triples(
|
|
|
|
|
"urn:step", "urn:plan", "Define X",
|
|
|
|
|
)
|
|
|
|
|
assert _has_type(triples, "urn:step", PROV_ENTITY)
|
|
|
|
|
assert _has_type(triples, "urn:step", TG_STEP_RESULT)
|
|
|
|
|
assert _has_type(triples, "urn:step", TG_ANSWER_TYPE)
|
|
|
|
|
|
|
|
|
|
def test_links_to_plan(self):
|
|
|
|
|
triples = agent_step_result_triples(
|
|
|
|
|
"urn:step", "urn:plan", "Define X",
|
|
|
|
|
)
|
|
|
|
|
ts = _triple_set(triples)
|
|
|
|
|
assert ("urn:step", PROV_WAS_DERIVED_FROM, "urn:plan") in ts
|
|
|
|
|
|
|
|
|
|
def test_includes_goal(self):
|
|
|
|
|
triples = agent_step_result_triples(
|
|
|
|
|
"urn:step", "urn:plan", "Define X",
|
|
|
|
|
)
|
|
|
|
|
values = _get_values(triples, "urn:step", TG_PLAN_STEP)
|
|
|
|
|
assert "Define X" in values
|
|
|
|
|
|
|
|
|
|
def test_includes_document_when_provided(self):
|
|
|
|
|
triples = agent_step_result_triples(
|
|
|
|
|
"urn:step", "urn:plan", "goal",
|
|
|
|
|
document_id="urn:doc/step",
|
|
|
|
|
)
|
|
|
|
|
values = _get_values(triples, "urn:step", TG_DOCUMENT)
|
|
|
|
|
assert "urn:doc/step" in values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSynthesisTriples:
|
|
|
|
|
|
|
|
|
|
def test_has_correct_types(self):
|
|
|
|
|
triples = agent_synthesis_triples(
|
|
|
|
|
"urn:synthesis", "urn:previous",
|
|
|
|
|
)
|
|
|
|
|
assert _has_type(triples, "urn:synthesis", PROV_ENTITY)
|
|
|
|
|
assert _has_type(triples, "urn:synthesis", TG_SYNTHESIS)
|
|
|
|
|
assert _has_type(triples, "urn:synthesis", TG_ANSWER_TYPE)
|
|
|
|
|
|
|
|
|
|
def test_links_to_previous(self):
|
|
|
|
|
triples = agent_synthesis_triples(
|
|
|
|
|
"urn:synthesis", "urn:last-finding",
|
|
|
|
|
)
|
|
|
|
|
ts = _triple_set(triples)
|
|
|
|
|
assert ("urn:synthesis", PROV_WAS_DERIVED_FROM,
|
|
|
|
|
"urn:last-finding") in ts
|
|
|
|
|
|
|
|
|
|
def test_includes_document_when_provided(self):
|
|
|
|
|
triples = agent_synthesis_triples(
|
|
|
|
|
"urn:synthesis", "urn:previous",
|
|
|
|
|
document_id="urn:doc/synthesis",
|
|
|
|
|
)
|
|
|
|
|
values = _get_values(triples, "urn:synthesis", TG_DOCUMENT)
|
|
|
|
|
assert "urn:doc/synthesis" in values
|
|
|
|
|
|
|
|
|
|
def test_label_is_synthesis(self):
|
|
|
|
|
triples = agent_synthesis_triples(
|
|
|
|
|
"urn:synthesis", "urn:previous",
|
|
|
|
|
)
|
|
|
|
|
labels = _get_values(triples, "urn:synthesis", RDFS_LABEL)
|
|
|
|
|
assert "Synthesis" in labels
|