Skip to content

v1.2.0 — Tool-Boundary Guardrails Complete & Structured Synthesis Follow-ups

Choose a tag to compare

@johnnichev johnnichev released this 14 Jul 17:59
· 7 commits to main since this release
068cd67

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 the GuardrailError once 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(), and astream().

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-level required keys), 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 with structured_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 as GuardrailError.agent_trace.
  • Async observers get a_on_guardrail_triggered on all stages including blocks; AuditLogger writes guardrail_triggered JSONL 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.