fix: normalize optional fields in tool invocation identity (#4289)

This commit is contained in:
Kazuhiro Sera
2026-08-08 07:38:36 +09:00
committed by GitHub
parent fb3a2482ea
commit 54ab78ec1d
8 changed files with 97 additions and 12 deletions
@@ -12,9 +12,12 @@ VERIFIER_PROMPT = (
"URLs. Judge the report only against that supplied evidence; do not reject or approve claims "
"based on your own memory. Check that material numeric and time-sensitive claims are supported "
"by the evidence, that citations use supplied URLs, that the report is internally consistent, "
"and that uncertainty is appropriately caveated. Treat information published on or before the "
"research cutoff as potentially available. Mark unsupported claims separately from claims that "
"the evidence directly contradicts."
"and that uncertainty is appropriately caveated. The payload includes allowed_source_urls; "
"compare citation URL strings exactly against that list. If a citation exactly matches an "
"allowed URL, accept the URL even when it contains a tracking parameter or another allowed "
"variant exists. Treat information published on or before the research cutoff as potentially "
"available. Mark unsupported claims separately from claims that the evidence directly "
"contradicts."
)
@@ -17,7 +17,9 @@ WRITER_PROMPT = (
REVISION_PROMPT = (
f"{WRITER_PROMPT} You are revising an existing report after evidence verification. Address "
"every verification issue, remove claims that cannot be supported, preserve valid analysis, "
"and return a complete replacement report rather than a patch or commentary."
"and return a complete replacement report rather than a patch or commentary. Copy replacement "
"citation URLs verbatim from each verification issue's source_urls; do not change path casing, "
"language segments, or query parameters."
)
+17 -4
View File
@@ -237,18 +237,31 @@ class FinancialResearchManager:
search_results: Sequence[FinancialSearchEvidence],
) -> VerificationResult:
self.printer.update_item("verifying", "Verifying report...")
input_data = json.dumps(
result = await Runner.run(
verifier_agent,
self._verification_input(query, report, search_results),
)
self.printer.mark_item_done("verifying")
return result.final_output_as(VerificationResult)
def _verification_input(
self,
query: str,
report: FinancialReportData,
search_results: Sequence[FinancialSearchEvidence],
) -> str:
return json.dumps(
{
"original_query": query,
"research_cutoff": self.research_cutoff,
"report": report.model_dump(mode="json"),
"evidence": [item.model_dump(mode="json") for item in search_results],
"allowed_source_urls": sorted(
{source.url for item in search_results for source in item.sources}
),
},
ensure_ascii=False,
)
result = await Runner.run(verifier_agent, input_data)
self.printer.mark_item_done("verifying")
return result.final_output_as(VerificationResult)
def _report_input(
self,
+2
View File
@@ -79,6 +79,8 @@ DEFAULT_AUTO_SKIP = {
"examples/sandbox/docker/mounts/gcs_mount_read_write.py",
"examples/sandbox/docker/mounts/s3_files_mount_read_write.py",
"examples/sandbox/docker/mounts/s3_mount_read_write.py",
# Blaxel 0.3.2 still imports an MCP v1 module that was removed in MCP v2.
"examples/sandbox/extensions/blaxel_runner.py",
"examples/sandbox/extensions/daytona/usaspending_text2sql/setup_db.py",
"examples/sandbox/extensions/temporal/temporal_sandbox_agent.py",
# Temporarily disabled due to credential issues.
+9 -4
View File
@@ -72,15 +72,16 @@ def _as_mapping(value: Any) -> Mapping[str, Any] | None:
return None
def _normalize_value(value: Any) -> Any:
def _normalize_value(value: Any, *, exclude_none: bool = False) -> Any:
mapping = _as_mapping(value)
if mapping is not None:
return {
str(key): _normalize_value(item)
str(key): _normalize_value(item, exclude_none=exclude_none)
for key, item in sorted(mapping.items(), key=lambda pair: str(pair[0]))
if not (exclude_none and item is None)
}
if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
return [_normalize_value(item) for item in value]
return [_normalize_value(item, exclude_none=exclude_none) for item in value]
if value is None or isinstance(value, str | int | float | bool):
return value
return str(value)
@@ -197,8 +198,12 @@ def tool_invocation_identity_and_scope(
if field_name not in mapping:
continue
value = mapping[field_name]
if value is None:
continue
semantic_payload[field_name] = (
_normalize_arguments(value) if field_name == "arguments" else _normalize_value(value)
_normalize_arguments(value)
if field_name == "arguments"
else _normalize_value(value, exclude_none=True)
)
return (
+24
View File
@@ -279,6 +279,30 @@ def test_financial_report_input_includes_cutoff_and_evidence() -> None:
}
def test_financial_verification_input_lists_exact_allowed_source_urls() -> None:
manager = object.__new__(FinancialResearchManager)
manager.research_cutoff = "2026-07-11"
source_url = "https://example.com/report?utm_source=openai"
evidence = FinancialSearchEvidence(
query="company annual report",
reason="Ground annual metrics",
summary="Revenue increased.",
sources=[FinancialSource(title="Annual report", url=source_url)],
retrieved_at="2026-07-11",
)
report = FinancialReportData(
short_summary="Summary",
markdown_report=f"Revenue increased ([source]({source_url})).",
follow_up_questions=[],
)
payload = json.loads(manager._verification_input("Analyze the company", report, [evidence]))
assert payload["allowed_source_urls"] == [source_url]
assert payload["report"] == report.model_dump(mode="json")
assert payload["evidence"] == [evidence.model_dump(mode="json")]
def test_sandbox_basic_direct_run_imports_external_docker_sdk(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
+1
View File
@@ -11,6 +11,7 @@ def test_default_auto_skip_excludes_prerequisite_bound_examples() -> None:
"examples/sandbox/docker/mounts/gcs_mount_read_write.py",
"examples/sandbox/docker/mounts/s3_files_mount_read_write.py",
"examples/sandbox/docker/mounts/s3_mount_read_write.py",
"examples/sandbox/extensions/blaxel_runner.py",
"examples/sandbox/extensions/daytona/usaspending_text2sql/setup_db.py",
"examples/sandbox/extensions/temporal/temporal_sandbox_agent.py",
"examples/sandbox/extensions/vercel_runner.py",
+35
View File
@@ -79,6 +79,41 @@ def test_canonical_shell_identity_ignores_stripped_provider_metadata() -> None:
assert tool_invocation_identity(provider_call) == tool_invocation_identity(persisted_call)
def test_canonical_shell_identity_treats_optional_nulls_as_omitted() -> None:
provider_call = {
"type": "shell_call",
"call_id": "shell_0",
"action": {
"commands": ["echo safe"],
"max_output_length": None,
"timeout_ms": None,
},
"environment": None,
}
normalized_call = dict(provider_call)
normalized_call["action"] = {"commands": ["echo safe"]}
normalized_call.pop("environment")
assert tool_invocation_identity(provider_call) == tool_invocation_identity(normalized_call)
def test_canonical_function_identity_preserves_null_argument_values() -> None:
call_with_null = {
"type": "function_call",
"call_id": "call_0",
"name": "lookup",
"arguments": '{"value": null}',
}
call_without_value = {
"type": "function_call",
"call_id": "call_0",
"name": "lookup",
"arguments": "{}",
}
assert tool_invocation_identity(call_with_null) != tool_invocation_identity(call_without_value)
@pytest.mark.asyncio
@pytest.mark.parametrize("fallback_type", ["custom", "function"])
async def test_completed_apply_patch_fallback_run_state_round_trip(fallback_type: str) -> None: