Repository navigation
v1.2.0 — Tool-Boundary Guardrails Complete & Structured Synthesis Follow-ups
Second 1.x minor, closing the loop on v1.1.0's guardrail and structured-output work from real tool-heavy agentic chat usage (#164–#167). All additive; the frozen 1.0 API is unchanged. Every feature went through a high-effort adversarial multi-agent review before merge — 28 verified findings fixed with red-first regression tests.
Tool-results guardrails (#165 → #168)
tool_args (v1.1) gated what goes INTO a tool; tool_results gates what comes OUT — external API responses, retrieved RAG chunks, oversized blobs — before it re-enters the model context:
config = AgentConfig(
guardrails=GuardrailsPipeline(
tool_results=[PIIGuardrail(), LengthGuardrail(max_chars=50_000)],
),
)- Blocks contain instead of corrupting state: a blocked result is replaced with a
[Tool result blocked by guardrail ...]marker so history/memory stay coherent (no dangling tool_call to 400 the next turn), terminal observer events fire, and parallel siblings persist — then the loop raises theGuardrailErroronce the batch is processed. The blocked content never reaches the model, observers, memory, or cache. - Guardrails run at use time: on fresh results AND on tool-result cache hits, so adding a guardrail applies retroactively to entries cached before it existed.
- Covers single and parallel execution across
run(),arun(), andastream().
Structured synthesis, now conditional (#164, #166 → #169)
final_turn_only no longer pays an extra full-context call on every turn:
reuse_loop_answer(default True): when the converged loop answer IS a single JSON object that validates (Pydantic fully; dict schemas must carry all top-levelrequiredkeys), it becomes the structured answer directly — no synthesis call.should_finalize=(messages, text) -> bool: skip the synthesis turn for plain conversational replies; the run finishes withstructured_status="skipped". A validating answer always wins before the predicate runs.single_pass=True: providers supporting tools+json_schema (OpenAI/Azure) carry the schema natively on loop calls — the structured object comes from the final loop turn, zero extra round-trips.
Explicit outcome: AgentResult.structured_status ("ok" / "validation_failed" with structured_error / "skipped" / "not_attempted"; None when no response_format) — no more guessing what a bare parsed=None means. astream() emits StreamChunk(event="structured_synthesis_start") so chat clients can render a pending state.
Guardrail observability (#167 → #170)
Every guardrail trip on every stage fires through the standard observer infra:
def on_guardrail_triggered(self, run_id, stage, guardrail_name, action, detail=None): ...- Trips are
(name, action)pairs recorded by the chain itself — faithful for comma-containing and duplicate names. - Blocks emit the rewrite/warn trips that preceded them in the same chain (
GuardrailError.prior_trips) — audit trails never lose a PII redaction that ran before a topic block — and attach the run trace asGuardrailError.agent_trace. - Async observers get
a_on_guardrail_triggeredon all stages including blocks;AuditLoggerwritesguardrail_triggeredJSONL records. Content is never included in events.
Compatibility
- New symbols (
GuardrailsPipeline.tool_results,StructuredOutputConfig.reuse_loop_answer/should_finalize/single_pass,AgentResult.structured_status/structured_error,StreamChunk.event,GuardrailResult.trips,GuardrailError.prior_trips/agent_trace,on_guardrail_triggered) are all additive. - Pre-1.2 tool-result cache entries stay valid (guardrails now apply at read time).
Full suite: 7,594 tests green across Python 3.10–3.13. Thanks to @DhruvGarg111 for the builder pinned-ports fix (#157) also landing on main this cycle.