Build the per-call ``StructuredOutput`` tool injected into a schema subagent. Unlike the stock ``StructuredOutputTool`` (which neither validates nor returns its payload), this tool drives ``collector``: a valid emission is captured and acknowledged; an invalid one is returned as a tool
(collector: StructuredOutputCollector)
| 73 | |
| 74 | |
| 75 | def make_structured_output_tool(collector: StructuredOutputCollector) -> Tool: |
| 76 | """Build the per-call ``StructuredOutput`` tool injected into a schema subagent. |
| 77 | |
| 78 | Unlike the stock ``StructuredOutputTool`` (which neither validates nor |
| 79 | returns its payload), this tool drives ``collector``: a valid emission is |
| 80 | captured and acknowledged; an invalid one is returned as a tool *error* |
| 81 | carrying the validation message so the model corrects and retries, until the |
| 82 | retry cap is reached. The input schema is left permissive so the model's |
| 83 | raw object reaches :meth:`StructuredOutputCollector.offer` for validation |
| 84 | here (rather than being rejected earlier by dispatch-level validation). |
| 85 | """ |
| 86 | |
| 87 | def _call(tool_input: dict, context: Any) -> ToolResult: |
| 88 | accepted, error = collector.offer(tool_input) |
| 89 | if accepted: |
| 90 | outbox = getattr(context, "outbox", None) |
| 91 | if outbox is not None: |
| 92 | outbox.append({"tool": SYNTHETIC_OUTPUT_TOOL_NAME, "structured_output": tool_input}) |
| 93 | return ToolResult( |
| 94 | name=SYNTHETIC_OUTPUT_TOOL_NAME, |
| 95 | output={"data": "Structured output accepted.", "structured_output": tool_input}, |
| 96 | ) |
| 97 | if collector.exhausted: |
| 98 | return ToolResult( |
| 99 | name=SYNTHETIC_OUTPUT_TOOL_NAME, |
| 100 | output={"data": f"Structured output failed validation after {collector.attempts} attempts: {error}"}, |
| 101 | is_error=True, |
| 102 | ) |
| 103 | return ToolResult( |
| 104 | name=SYNTHETIC_OUTPUT_TOOL_NAME, |
| 105 | output={"data": f"Output did not match the schema: {error}. Fix the fields and call StructuredOutput again."}, |
| 106 | is_error=True, |
| 107 | ) |
| 108 | |
| 109 | from src.permissions.types import PermissionAllowDecision |
| 110 | |
| 111 | return build_tool( |
| 112 | name=SYNTHETIC_OUTPUT_TOOL_NAME, |
| 113 | input_schema={"type": "object", "additionalProperties": True}, |
| 114 | call=_call, |
| 115 | prompt=( |
| 116 | "Return your final answer by calling this tool exactly once at the end, " |
| 117 | "with arguments matching the requested schema." |
| 118 | ), |
| 119 | description="Return a final response as schema-validated structured JSON.", |
| 120 | max_result_size_chars=100_000, |
| 121 | is_read_only=lambda _input: True, |
| 122 | is_concurrency_safe=lambda _input: True, |
| 123 | # Always allowed — it only records the model's own final answer; without |
| 124 | # this the subagent's permission context can block it before validation. |
| 125 | check_permissions=lambda tool_input, _ctx: PermissionAllowDecision(updated_input=tool_input), |
| 126 | ) |