From 0ac9b958f3c7a7d901458e061b5301ac257a132e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=82=E9=A9=AC?= Date: Thu, 9 Jul 2026 14:40:55 +0800 Subject: [PATCH 1/4] fix: trust transcript artifact directories --- src/iac_code/pipeline/engine/step_executor.py | 12 ++- src/iac_code/services/session_layout.py | 3 + .../engine/test_transcript_storage.py | 76 +++++++++++++++++++ tests/services/test_session_layout.py | 3 + 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/iac_code/pipeline/engine/step_executor.py b/src/iac_code/pipeline/engine/step_executor.py index a16c7df1..f4b20143 100644 --- a/src/iac_code/pipeline/engine/step_executor.py +++ b/src/iac_code/pipeline/engine/step_executor.py @@ -415,6 +415,7 @@ def build_agent_loop_context( agent_session_id = transcript_id or session_id session_usage_store = None result_storage_dir = None + transcript_image_cache_dir = None audit_log_path = None if ( transcript_id @@ -429,6 +430,10 @@ def build_agent_loop_context( root_session_dir, session_paths.transcript_tool_results_dir(transcript_id), ) + transcript_image_cache_dir = ensure_session_owned_dir( + root_session_dir, + session_paths.transcript_image_cache_dir(transcript_id), + ) audit_log_path = session_paths.transcript_permission_audit_path(transcript_id) usage_path = session_paths.transcript_usage_path(transcript_id) ensure_session_owned_parent(root_session_dir, audit_log_path) @@ -436,6 +441,11 @@ def build_agent_loop_context( assert transcript_dir == session_paths.transcript_dir(transcript_id) session_usage_store = SessionUsageStore(path_provider=lambda _cwd, _sid, path=usage_path: path) step_skill_roots = self._resolve_step_skill_roots(step) + trusted_read_roots = list(step_skill_roots) + if result_storage_dir is not None: + trusted_read_roots.append(str(result_storage_dir)) + if transcript_image_cache_dir is not None: + trusted_read_roots.append(str(transcript_image_cache_dir)) agent_loop = AgentLoop( provider_manager=self._provider_manager, system_prompt=system_prompt, @@ -453,7 +463,7 @@ def build_agent_loop_context( pause_event=self._pause_event, permission_context_getter=self._permission_context_getter, auto_trigger_skills=self._resolve_auto_trigger_skills(step), - tool_context_trusted_read_directories=step_skill_roots, + tool_context_trusted_read_directories=trusted_read_roots, tool_context_relative_read_directories=step_skill_roots, pipeline_mode=True, tool_context_env_overrides=self._tool_context_env_overrides, diff --git a/src/iac_code/services/session_layout.py b/src/iac_code/services/session_layout.py index e7cfd8be..17bc0476 100644 --- a/src/iac_code/services/session_layout.py +++ b/src/iac_code/services/session_layout.py @@ -198,3 +198,6 @@ def transcript_permission_audit_path(self, transcript_id: str) -> Path: def transcript_tool_results_dir(self, transcript_id: str) -> Path: return self.transcript_dir(transcript_id) / "tool-results" + + def transcript_image_cache_dir(self, transcript_id: str) -> Path: + return self.transcript_dir(transcript_id) / "image-cache" diff --git a/tests/pipeline/engine/test_transcript_storage.py b/tests/pipeline/engine/test_transcript_storage.py index d76b3f13..a38f2390 100644 --- a/tests/pipeline/engine/test_transcript_storage.py +++ b/tests/pipeline/engine/test_transcript_storage.py @@ -15,11 +15,13 @@ from iac_code.pipeline.engine.step_executor import StepExecutor from iac_code.pipeline.engine.step_spec import LoadedPipeline, StepSpec from iac_code.pipeline.engine.transcript_storage import PipelineTranscriptStorage +from iac_code.services.permissions.pipeline import check_tool_permission from iac_code.services.session_layout import SessionPaths, UnsupportedSessionLayoutError from iac_code.services.session_metadata import SESSION_LAYOUT_VERSION_V2, SessionMetadata, write_session_metadata from iac_code.services.session_storage import SessionStorage from iac_code.services.session_usage import SessionUsageStore from iac_code.tools.base import Tool, ToolContext, ToolRegistry, ToolResult +from iac_code.tools.bash.bash_tool import BashTool from iac_code.types.permissions import PermissionAuditMetadata, PermissionResult, ToolPermissionContext from iac_code.types.stream_events import ( MessageEndEvent, @@ -397,6 +399,80 @@ async def test_step_executor_routes_transcript_runtime_paths(tmp_path: Path): assert permission_event.audit_context["audit_log_path"] == str(transcript_dir / "permission-audit.jsonl") +@pytest.mark.asyncio +async def test_step_executor_trusts_current_transcript_artifacts_for_bash_reads(tmp_path: Path): + (tmp_path / "prompts").mkdir() + (tmp_path / "prompts" / "step.md").write_text("Run step.", encoding="utf-8") + root_storage = SessionStorage(projects_dir=tmp_path / "projects") + root_session_dir = root_storage.session_dir("/repo", "root-session") + write_session_metadata( + root_session_dir, + SessionMetadata(session_id="root-session", cwd="/repo", layout_version=SESSION_LAYOUT_VERSION_V2), + ) + transcript_storage = PipelineTranscriptStorage(tmp_path / "detached-pipeline") + step = StepSpec(step_id="step", conclusion_field="out", forward=None, prompt_file="prompts/step.md") + pipeline = LoadedPipeline( + name="test", + steps=[step], + context_dependencies={"out": []}, + max_rollbacks=1, + skills={}, + ) + executor = StepExecutor( + provider_manager=_FakeProvider(), + base_tool_registry=ToolRegistry(), + pipeline=pipeline, + pipeline_dir=tmp_path, + session_storage=transcript_storage, + root_session_storage=root_storage, + cwd="/repo", + permission_context_getter=lambda: ToolPermissionContext(cwd="/repo"), + ) + + agent_context = executor.build_agent_loop_context( + step, + PipelineContext({"out": []}), + "root-session", + transcript_id="transcript_att_0001", + ) + + loop = agent_context.agent_loop + assert loop is not None + session_paths = SessionPaths.require_supported(root_session_dir) + transcript_tool_results_dir = session_paths.transcript_tool_results_dir("transcript_att_0001") + transcript_image_cache_dir = session_paths.transcript_image_cache_dir("transcript_att_0001") + result_file = transcript_tool_results_dir / "tool-1.txt" + result_file.parent.mkdir(parents=True, exist_ok=True) + result_file.write_text("saved result", encoding="utf-8") + image_file = transcript_image_cache_dir / "1.png" + image_file.parent.mkdir(parents=True, exist_ok=True) + image_file.write_bytes(b"png") + + result_permission = await check_tool_permission( + BashTool(), + {"command": f"cat {result_file}"}, + ToolPermissionContext( + cwd="/repo", + trusted_read_directories=list(loop._tool_context_trusted_read_directories), + ), + ) + image_permission = await check_tool_permission( + BashTool(), + {"command": f"cat {image_file}"}, + ToolPermissionContext( + cwd="/repo", + trusted_read_directories=list(loop._tool_context_trusted_read_directories), + ), + ) + + assert str(transcript_tool_results_dir) in loop._tool_context_trusted_read_directories + assert str(transcript_image_cache_dir) in loop._tool_context_trusted_read_directories + assert str(root_session_dir) not in loop._tool_context_trusted_read_directories + assert str(transcript_tool_results_dir.parent) not in loop._tool_context_trusted_read_directories + assert result_permission.behavior == "allow" + assert image_permission.behavior == "allow" + + def test_step_executor_keeps_legacy_root_on_legacy_runtime_paths(tmp_path: Path): (tmp_path / "prompts").mkdir() (tmp_path / "prompts" / "step.md").write_text("Run step.", encoding="utf-8") diff --git a/tests/services/test_session_layout.py b/tests/services/test_session_layout.py index 6fe3d804..3e1e0a34 100644 --- a/tests/services/test_session_layout.py +++ b/tests/services/test_session_layout.py @@ -55,6 +55,9 @@ def test_layout_v2_paths_are_session_scoped(tmp_path: Path) -> None: assert paths.transcript_tool_results_dir("transcript_att_0001") == ( session_dir / "pipeline" / "transcripts" / "transcript_att_0001" / "tool-results" ) + assert paths.transcript_image_cache_dir("transcript_att_0001") == ( + session_dir / "pipeline" / "transcripts" / "transcript_att_0001" / "image-cache" + ) @pytest.mark.parametrize( From 03d77a94fa3202b1b1514f84d6bd26aba98c9fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=82=E9=A9=AC?= Date: Thu, 9 Jul 2026 15:15:52 +0800 Subject: [PATCH 2/4] fix: compact complete_step validation output --- src/iac_code/agent/agent_loop.py | 17 +++-- .../i18n/locales/de/LC_MESSAGES/messages.po | 4 ++ .../i18n/locales/es/LC_MESSAGES/messages.po | 4 ++ .../i18n/locales/fr/LC_MESSAGES/messages.po | 4 ++ .../i18n/locales/ja/LC_MESSAGES/messages.po | 4 ++ .../i18n/locales/pt/LC_MESSAGES/messages.po | 4 ++ .../i18n/locales/zh/LC_MESSAGES/messages.po | 4 ++ .../pipeline/engine/complete_step_tool.py | 7 ++ src/iac_code/ui/renderer.py | 6 ++ tests/agent/test_agent_loop_new.py | 6 ++ .../engine/test_complete_step_tool.py | 14 ++++ tests/ui/test_renderer_helpers.py | 72 ++++++++++++++++++- 12 files changed, 140 insertions(+), 6 deletions(-) diff --git a/src/iac_code/agent/agent_loop.py b/src/iac_code/agent/agent_loop.py index 7c888643..b1f063e4 100644 --- a/src/iac_code/agent/agent_loop.py +++ b/src/iac_code/agent/agent_loop.py @@ -1276,7 +1276,7 @@ async def poll_event_queues(): tool_use_id=req.id, content=processed.content, is_error=result.is_error, - metadata=self._tool_result_block_metadata(processed), + metadata=self._tool_result_block_metadata(processed, result_metadata), ) ) @@ -1406,10 +1406,17 @@ def _tool_result_render_metadata( return cls._merge_tool_render_metadata(metadata, render_metadata) @staticmethod - def _tool_result_block_metadata(processed: Any) -> dict[str, Any]: - if not getattr(processed, "is_externalized", False) or not getattr(processed, "file_path", None): - return {} - return {EXTERNALIZED_RESULT_PATH_METADATA_KEY: str(processed.file_path)} + def _tool_result_block_metadata(processed: Any, result_metadata: dict[str, Any] | None = None) -> dict[str, Any]: + metadata: dict[str, Any] = {} + if getattr(processed, "is_externalized", False) and getattr(processed, "file_path", None): + metadata[EXTERNALIZED_RESULT_PATH_METADATA_KEY] = str(processed.file_path) + + if isinstance(result_metadata, dict): + render_metadata = result_metadata.get(TOOL_RENDER_METADATA_KEY) + if isinstance(render_metadata, dict): + metadata[TOOL_RENDER_METADATA_KEY] = dict(render_metadata) + + return metadata def _mark_read_memory_tool_result(self, request: ToolCallRequest, result: ToolResult) -> None: if request.name != "read_memory" or result.is_error or self._memory_recall_service is None: diff --git a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po index 991fcd6c..28565a56 100644 --- a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po @@ -3365,6 +3365,10 @@ msgstr "conclusion muss dieser Schema-Zusammenfassung entsprechen:\n" msgid "" msgstr "" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "complete_step validation failed." +msgstr "Die Validierung von complete_step ist fehlgeschlagen." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "Clarification is required before completing the current step." msgstr "Vor Abschluss des aktuellen Schritts ist eine Klärung erforderlich." diff --git a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po index d9b6b60c..fde907d2 100644 --- a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po @@ -3357,6 +3357,10 @@ msgstr "conclusion debe coincidir con este resumen de esquema:\n" msgid "" msgstr "" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "complete_step validation failed." +msgstr "La validación de complete_step falló." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "Clarification is required before completing the current step." msgstr "Se requiere una aclaración antes de completar el paso actual." diff --git a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po index 34f1c3d5..1b669db8 100644 --- a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po @@ -3364,6 +3364,10 @@ msgstr "conclusion doit correspondre à ce résumé de schéma :\n" msgid "" msgstr "" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "complete_step validation failed." +msgstr "La validation de complete_step a échoué." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "Clarification is required before completing the current step." msgstr "Une clarification est requise avant de terminer l’étape actuelle." diff --git a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po index 15156437..6c408692 100644 --- a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po @@ -3200,6 +3200,10 @@ msgstr "conclusion は次のスキーマ概要に一致する必要がありま msgid "" msgstr "<現在のステップ要件に従って入力>" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "complete_step validation failed." +msgstr "complete_step の検証に失敗しました。" + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "Clarification is required before completing the current step." msgstr "現在のステップを完了する前に確認が必要です。" diff --git a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po index 8b2ae21a..beda3c52 100644 --- a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po @@ -3334,6 +3334,10 @@ msgstr "conclusion deve corresponder a este resumo de esquema:\n" msgid "" msgstr "" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "complete_step validation failed." +msgstr "A validação de complete_step falhou." + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "Clarification is required before completing the current step." msgstr "É necessário esclarecimento antes de concluir a etapa atual." diff --git a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po index bfa8aa09..24f8482a 100644 --- a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po @@ -3166,6 +3166,10 @@ msgstr "conclusion 必须符合以下 schema 摘要:\n" msgid "" msgstr "<按当前步骤要求填写>" +#: src/iac_code/pipeline/engine/complete_step_tool.py +msgid "complete_step validation failed." +msgstr "complete_step 校验失败。" + #: src/iac_code/pipeline/engine/complete_step_tool.py msgid "Clarification is required before completing the current step." msgstr "完成当前步骤前需要先澄清。" diff --git a/src/iac_code/pipeline/engine/complete_step_tool.py b/src/iac_code/pipeline/engine/complete_step_tool.py index a33b9958..86040c17 100644 --- a/src/iac_code/pipeline/engine/complete_step_tool.py +++ b/src/iac_code/pipeline/engine/complete_step_tool.py @@ -301,6 +301,13 @@ def _example_from_schema(cls, schema: Any) -> Any: def is_read_only(self, input: dict | None = None) -> bool: return True + def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False) -> str | None: + if not output or not is_error: + return None + if verbose: + return output.strip() + return _("complete_step validation failed.") + def _validate_conclusion(self, conclusion: dict) -> str | None: """Validate conclusion against schema. Returns error message or None.""" schema = self._step_config.conclusion_schema diff --git a/src/iac_code/ui/renderer.py b/src/iac_code/ui/renderer.py index 5ca3cf7a..1d692aa6 100644 --- a/src/iac_code/ui/renderer.py +++ b/src/iac_code/ui/renderer.py @@ -101,6 +101,11 @@ def _known_tool_result_message( is_error: bool = False, verbose: bool = False, ) -> str | None: + if tool_name == "complete_step" and is_error: + if verbose: + return output.strip() + return _("complete_step validation failed.") + if tool_name == "ros_deploy": from iac_code.pipeline.selling.tools.ros_deploy_tool import RosDeployTool @@ -2090,6 +2095,7 @@ def replay_history(self, messages: list) -> None: result=result.content if result else None, is_error=result.is_error if result else False, done=True, + metadata=copy.deepcopy(result.metadata) if result else None, ) segments.append(_Segment(kind="tool", tool=rec)) if segments: diff --git a/tests/agent/test_agent_loop_new.py b/tests/agent/test_agent_loop_new.py index 4b6080c9..5ea8d3e3 100644 --- a/tests/agent/test_agent_loop_new.py +++ b/tests/agent/test_agent_loop_new.py @@ -1852,6 +1852,12 @@ async def fake_stream(messages, system, tools=None, max_tokens=8192): TOOL_RENDER_RESULT_COMPACT_KEY: "compact result", } } + result_blocks = loop.context_manager.add_tool_results.call_args.args[0] + assert result_blocks[0].metadata == { + TOOL_RENDER_METADATA_KEY: { + TOOL_RENDER_RESULT_COMPACT_KEY: "compact result", + } + } tool_starts = [e for e in events if isinstance(e, ToolUseStartEvent)] assert len(tool_starts) == 1 assert not hasattr(tool_starts[0], "renderer_tool") diff --git a/tests/pipeline/engine/test_complete_step_tool.py b/tests/pipeline/engine/test_complete_step_tool.py index 995b8902..95079ff9 100644 --- a/tests/pipeline/engine/test_complete_step_tool.py +++ b/tests/pipeline/engine/test_complete_step_tool.py @@ -32,6 +32,20 @@ def test_has_input_schema(self, tool): assert "conclusion" in schema["properties"] assert "conclusion" in schema["required"] + def test_error_result_renders_compact_summary(self, tool): + long_error = ( + "Invalid input for tool 'complete_step': 'conclusion' is a required property\n" + "Current step: intent_parsing\n" + "conclusion must match this schema summary:\n" + '{"type": "object", "required": ["is_infra_intent", "confidence"]}' + ) + + compact = tool.render_tool_result_message(long_error, is_error=True) + + assert compact == "complete_step validation failed." + assert "'conclusion' is a required property" not in compact + assert "schema summary" not in compact + @pytest.mark.asyncio async def test_completion_guard_message_key_renders_translated_message(self, step_config): tool = CompleteStepTool( diff --git a/tests/ui/test_renderer_helpers.py b/tests/ui/test_renderer_helpers.py index b44c3879..074e8166 100644 --- a/tests/ui/test_renderer_helpers.py +++ b/tests/ui/test_renderer_helpers.py @@ -8,7 +8,14 @@ import pytest from rich.console import Console -from iac_code.agent.message import ImageBlock, Message, TextBlock, create_recalled_memory_message +from iac_code.agent.message import ( + ImageBlock, + Message, + TextBlock, + ToolResultBlock, + ToolUseBlock, + create_recalled_memory_message, +) from iac_code.pipeline.engine.cleanup import CLEANUP_PROMPT_METADATA_TYPE from iac_code.tools.base import Tool, ToolContext, ToolRegistry, ToolResult from iac_code.tools.read_file import ReadFileTool @@ -306,6 +313,69 @@ def test_render_tool_result_uses_record_render_metadata_when_registry_missing(se assert "Command: infraguard scan" in verbose.plain assert '{"command"' not in verbose.plain + def test_replay_history_uses_tool_result_block_render_metadata_when_registry_missing(self): + renderer = Renderer(make_console(), ToolRegistry(), status_callback=lambda: "ready") + long_error = ( + "Invalid input for tool 'complete_step': 'conclusion' is a required property\n" + "Current step: intent_parsing\n" + "conclusion must match this schema summary:\n" + '{"type": "object", "required": ["is_infra_intent", "confidence"]}' + ) + messages = [ + Message( + role="assistant", + content=[ToolUseBlock(id="tu_bad", name="complete_step", input={})], + ), + Message( + role="user", + content=[ + ToolResultBlock( + tool_use_id="tu_bad", + content=long_error, + is_error=True, + metadata={ + TOOL_RENDER_METADATA_KEY: { + TOOL_RENDER_RESULT_COMPACT_KEY: "complete_step validation failed." + } + }, + ) + ], + ), + ] + + renderer.replay_history(messages) + + output = renderer.console.file.getvalue() + assert "complete_step validation failed." in output + assert "'conclusion' is a required property" not in output + assert "schema summary" not in output + + def test_replay_history_summarizes_legacy_complete_step_error_without_metadata(self): + renderer = Renderer(make_console(), ToolRegistry(), status_callback=lambda: "ready") + long_error = ( + "Invalid input for tool 'complete_step': 'conclusion' is a required property\n" + "Current step: intent_parsing\n" + "conclusion must match this schema summary:\n" + '{"type": "object", "required": ["is_infra_intent", "confidence"]}' + ) + messages = [ + Message( + role="assistant", + content=[ToolUseBlock(id="tu_bad", name="complete_step", input={})], + ), + Message( + role="user", + content=[ToolResultBlock(tool_use_id="tu_bad", content=long_error, is_error=True)], + ), + ] + + renderer.replay_history(messages) + + output = renderer.console.file.getvalue() + assert "complete_step validation failed." in output + assert "'conclusion' is a required property" not in output + assert "schema summary" not in output + def test_render_progress_groups_include_resource_rows(self): renderer = make_renderer() From 988ce7bac3db793d23d4948cad51f5392f4bca5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=82=E9=A9=AC?= Date: Thu, 9 Jul 2026 19:24:47 +0800 Subject: [PATCH 3/4] fix: harden tool render metadata --- src/iac_code/agent/agent_loop.py | 15 ++++++++++++++- src/iac_code/tools/cloud/base_api.py | 10 +++++++++- tests/agent/test_agent_loop_new.py | 28 ++++++++++++++++++++++++++++ tests/tools/cloud/test_base_api.py | 9 +++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/iac_code/agent/agent_loop.py b/src/iac_code/agent/agent_loop.py index b1f063e4..bb7b9700 100644 --- a/src/iac_code/agent/agent_loop.py +++ b/src/iac_code/agent/agent_loop.py @@ -1414,7 +1414,20 @@ def _tool_result_block_metadata(processed: Any, result_metadata: dict[str, Any] if isinstance(result_metadata, dict): render_metadata = result_metadata.get(TOOL_RENDER_METADATA_KEY) if isinstance(render_metadata, dict): - metadata[TOOL_RENDER_METADATA_KEY] = dict(render_metadata) + safe_render_metadata: dict[str, Any] = {} + for key in ( + TOOL_RENDER_DISPLAY_NAME_KEY, + TOOL_RENDER_RESULT_COMPACT_KEY, + TOOL_RENDER_RESULT_VERBOSE_KEY, + ): + value = render_metadata.get(key) + if isinstance(value, str): + safe_render_metadata[key] = value + value = render_metadata.get(TOOL_RENDER_VERBOSE_RESULT_IN_TRANSCRIPT_KEY) + if isinstance(value, bool): + safe_render_metadata[TOOL_RENDER_VERBOSE_RESULT_IN_TRANSCRIPT_KEY] = value + if safe_render_metadata: + metadata[TOOL_RENDER_METADATA_KEY] = safe_render_metadata return metadata diff --git a/src/iac_code/tools/cloud/base_api.py b/src/iac_code/tools/cloud/base_api.py index 06622023..0830ec89 100644 --- a/src/iac_code/tools/cloud/base_api.py +++ b/src/iac_code/tools/cloud/base_api.py @@ -139,7 +139,15 @@ def render_tool_result_message(self, output: str, *, is_error: bool = False, ver if verbose: return output.strip() action = getattr(self, "_last_action", "") - result = getattr(self, "_last_result", None) + result = None + try: + parsed = json.loads(output) + except (TypeError, ValueError): + parsed = None + if isinstance(parsed, dict): + result = parsed + else: + result = getattr(self, "_last_result", None) if action and result is not None: return self._summarize_success_result(action, result) lines = output.strip().splitlines() diff --git a/tests/agent/test_agent_loop_new.py b/tests/agent/test_agent_loop_new.py index 5ea8d3e3..9eaa6074 100644 --- a/tests/agent/test_agent_loop_new.py +++ b/tests/agent/test_agent_loop_new.py @@ -14,6 +14,8 @@ TOOL_RENDER_DISPLAY_NAME_KEY, TOOL_RENDER_METADATA_KEY, TOOL_RENDER_RESULT_COMPACT_KEY, + TOOL_RENDER_RESULT_VERBOSE_KEY, + TOOL_RENDER_VERBOSE_RESULT_IN_TRANSCRIPT_KEY, CompactionEvent, MessageEndEvent, MessageStartEvent, @@ -101,6 +103,32 @@ def get_stats_snapshot(self): assert recall_service.surfaced == {"ros-yaml.md"} +def test_tool_result_block_metadata_filters_render_metadata_to_json_safe_fields(): + sentinel = object() + + metadata = AgentLoop._tool_result_block_metadata( + SimpleNamespace(is_externalized=False), + { + TOOL_RENDER_METADATA_KEY: { + TOOL_RENDER_DISPLAY_NAME_KEY: "Custom tool", + TOOL_RENDER_RESULT_COMPACT_KEY: sentinel, + TOOL_RENDER_RESULT_VERBOSE_KEY: "Verbose output", + TOOL_RENDER_VERBOSE_RESULT_IN_TRANSCRIPT_KEY: True, + "plugin_payload": sentinel, + } + }, + ) + + assert metadata == { + TOOL_RENDER_METADATA_KEY: { + TOOL_RENDER_DISPLAY_NAME_KEY: "Custom tool", + TOOL_RENDER_RESULT_VERBOSE_KEY: "Verbose output", + TOOL_RENDER_VERBOSE_RESULT_IN_TRANSCRIPT_KEY: True, + } + } + json.dumps(metadata) + + class TestAgentLoopInit: def test_init(self, mock_provider, mock_registry): loop = AgentLoop(provider_manager=mock_provider, system_prompt="test", tool_registry=mock_registry) diff --git a/tests/tools/cloud/test_base_api.py b/tests/tools/cloud/test_base_api.py index 397f2ecd..f9fd4765 100644 --- a/tests/tools/cloud/test_base_api.py +++ b/tests/tools/cloud/test_base_api.py @@ -191,3 +191,12 @@ def test_success_summary_uses_cached_action_and_result(self) -> None: api._last_result = {"Things": [{"Id": "1"}]} result = api.render_tool_result_message('{\n "Things": [{"Id": "1"}]\n}') assert result == "ListThings -> 1 fields" + + def test_success_summary_uses_current_output_over_cached_last_result(self) -> None: + api = MockCloudApiWithSummary() + api._last_action = "ListThings" + api._last_result = {"Stale": "last call"} + + result = api.render_tool_result_message('{"Current": true, "RequestId": "REQ-1"}') + + assert result == "ListThings -> 2 fields" From bd360bcdcd5d9ad544f0509a24916340ca255d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A1=82=E9=A9=AC?= Date: Thu, 9 Jul 2026 19:37:49 +0800 Subject: [PATCH 4/4] fix: compact ask_user_question validation output --- .../i18n/locales/de/LC_MESSAGES/messages.po | 5 ++++ .../i18n/locales/es/LC_MESSAGES/messages.po | 5 ++++ .../i18n/locales/fr/LC_MESSAGES/messages.po | 5 ++++ .../i18n/locales/ja/LC_MESSAGES/messages.po | 5 ++++ .../i18n/locales/pt/LC_MESSAGES/messages.po | 5 ++++ .../i18n/locales/zh/LC_MESSAGES/messages.po | 5 ++++ .../pipeline/engine/ask_user_question_tool.py | 9 +++++++ src/iac_code/ui/renderer.py | 9 +++++++ .../engine/test_ask_user_question_tool.py | 13 ++++++++++ tests/ui/test_renderer_helpers.py | 25 +++++++++++++++++++ 10 files changed, 86 insertions(+) diff --git a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po index 28565a56..eb73e04b 100644 --- a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po @@ -3069,6 +3069,10 @@ msgstr "" msgid "The user-facing question to ask." msgstr "Die dem Benutzer zu stellende Frage." +#: src/iac_code/pipeline/engine/ask_user_question_tool.py +msgid "ask_user_question validation failed." +msgstr "Die Validierung von ask_user_question ist fehlgeschlagen." + #: src/iac_code/pipeline/engine/ask_user_question_tool.py msgid "ask_user_question requires a pipeline event queue." msgstr "ask_user_question erfordert eine Pipeline-Ereigniswarteschlange." @@ -3366,6 +3370,7 @@ msgid "" msgstr "" #: src/iac_code/pipeline/engine/complete_step_tool.py +#: src/iac_code/ui/renderer.py msgid "complete_step validation failed." msgstr "Die Validierung von complete_step ist fehlgeschlagen." diff --git a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po index fde907d2..8e9e9878 100644 --- a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po @@ -3070,6 +3070,10 @@ msgstr "" msgid "The user-facing question to ask." msgstr "La pregunta que se mostrará al usuario." +#: src/iac_code/pipeline/engine/ask_user_question_tool.py +msgid "ask_user_question validation failed." +msgstr "La validación de ask_user_question falló." + #: src/iac_code/pipeline/engine/ask_user_question_tool.py msgid "ask_user_question requires a pipeline event queue." msgstr "ask_user_question requiere una cola de eventos del pipeline." @@ -3358,6 +3362,7 @@ msgid "" msgstr "" #: src/iac_code/pipeline/engine/complete_step_tool.py +#: src/iac_code/ui/renderer.py msgid "complete_step validation failed." msgstr "La validación de complete_step falló." diff --git a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po index 1b669db8..7f5a4bb3 100644 --- a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po @@ -3077,6 +3077,10 @@ msgstr "" msgid "The user-facing question to ask." msgstr "Question à poser à l’utilisateur." +#: src/iac_code/pipeline/engine/ask_user_question_tool.py +msgid "ask_user_question validation failed." +msgstr "La validation de ask_user_question a échoué." + #: src/iac_code/pipeline/engine/ask_user_question_tool.py msgid "ask_user_question requires a pipeline event queue." msgstr "ask_user_question nécessite une file d’événements du pipeline." @@ -3365,6 +3369,7 @@ msgid "" msgstr "" #: src/iac_code/pipeline/engine/complete_step_tool.py +#: src/iac_code/ui/renderer.py msgid "complete_step validation failed." msgstr "La validation de complete_step a échoué." diff --git a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po index 6c408692..83e9bca7 100644 --- a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po @@ -2958,6 +2958,10 @@ msgstr "ユーザーに選択肢の選択または確認内容の入力を求め msgid "The user-facing question to ask." msgstr "ユーザーに表示する質問です。" +#: src/iac_code/pipeline/engine/ask_user_question_tool.py +msgid "ask_user_question validation failed." +msgstr "ask_user_question の検証に失敗しました。" + #: src/iac_code/pipeline/engine/ask_user_question_tool.py msgid "ask_user_question requires a pipeline event queue." msgstr "ask_user_question にはパイプラインイベントキューが必要です。" @@ -3201,6 +3205,7 @@ msgid "" msgstr "<現在のステップ要件に従って入力>" #: src/iac_code/pipeline/engine/complete_step_tool.py +#: src/iac_code/ui/renderer.py msgid "complete_step validation failed." msgstr "complete_step の検証に失敗しました。" diff --git a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po index beda3c52..1c3f80e8 100644 --- a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po @@ -3053,6 +3053,10 @@ msgstr "" msgid "The user-facing question to ask." msgstr "A pergunta a ser exibida ao usuário." +#: src/iac_code/pipeline/engine/ask_user_question_tool.py +msgid "ask_user_question validation failed." +msgstr "A validação de ask_user_question falhou." + #: src/iac_code/pipeline/engine/ask_user_question_tool.py msgid "ask_user_question requires a pipeline event queue." msgstr "ask_user_question requer uma fila de eventos do pipeline." @@ -3335,6 +3339,7 @@ msgid "" msgstr "" #: src/iac_code/pipeline/engine/complete_step_tool.py +#: src/iac_code/ui/renderer.py msgid "complete_step validation failed." msgstr "A validação de complete_step falhou." diff --git a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po index 24f8482a..965914ca 100644 --- a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po @@ -2934,6 +2934,10 @@ msgstr "仅用于 pipeline 的工具,用于请用户选择选项或输入澄 msgid "The user-facing question to ask." msgstr "要询问用户的问题。" +#: src/iac_code/pipeline/engine/ask_user_question_tool.py +msgid "ask_user_question validation failed." +msgstr "ask_user_question 校验失败。" + #: src/iac_code/pipeline/engine/ask_user_question_tool.py msgid "ask_user_question requires a pipeline event queue." msgstr "ask_user_question 需要 pipeline 事件队列。" @@ -3167,6 +3171,7 @@ msgid "" msgstr "<按当前步骤要求填写>" #: src/iac_code/pipeline/engine/complete_step_tool.py +#: src/iac_code/ui/renderer.py msgid "complete_step validation failed." msgstr "complete_step 校验失败。" diff --git a/src/iac_code/pipeline/engine/ask_user_question_tool.py b/src/iac_code/pipeline/engine/ask_user_question_tool.py index 6992c978..477a116b 100644 --- a/src/iac_code/pipeline/engine/ask_user_question_tool.py +++ b/src/iac_code/pipeline/engine/ask_user_question_tool.py @@ -68,6 +68,15 @@ def is_concurrency_safe(self, tool_input: dict[str, Any]) -> bool: def needs_event_queue(self) -> bool: return True + def render_tool_result_message(self, output: str, *, is_error: bool = False, verbose: bool = False) -> str | None: + if not output or not is_error: + return None + if output.startswith("Invalid input for tool 'ask_user_question':"): + if verbose: + return output.strip() + return _("ask_user_question validation failed.") + return output.strip() + async def execute(self, *, tool_input: dict[str, Any], context: ToolContext) -> ToolResult: if context.event_queue is None: return ToolResult.error(_("ask_user_question requires a pipeline event queue.")) diff --git a/src/iac_code/ui/renderer.py b/src/iac_code/ui/renderer.py index 1d692aa6..9cedb06e 100644 --- a/src/iac_code/ui/renderer.py +++ b/src/iac_code/ui/renderer.py @@ -106,6 +106,15 @@ def _known_tool_result_message( return output.strip() return _("complete_step validation failed.") + if ( + tool_name == "ask_user_question" + and is_error + and output.startswith("Invalid input for tool 'ask_user_question':") + ): + if verbose: + return output.strip() + return _("ask_user_question validation failed.") + if tool_name == "ros_deploy": from iac_code.pipeline.selling.tools.ros_deploy_tool import RosDeployTool diff --git a/tests/pipeline/engine/test_ask_user_question_tool.py b/tests/pipeline/engine/test_ask_user_question_tool.py index 6c6212dd..679d6a6f 100644 --- a/tests/pipeline/engine/test_ask_user_question_tool.py +++ b/tests/pipeline/engine/test_ask_user_question_tool.py @@ -42,6 +42,19 @@ def test_schema_requires_question_and_options(self): assert option_schema["required"] == ["id", "label"] assert schema["additionalProperties"] is False + def test_validation_error_renders_compact_summary(self): + long_error = ( + "Invalid input for tool 'ask_user_question': " + "[{'id': 'tech_stack_nodejs', 'label': 'Node.js'}] is not of type 'object'. " + "Please provide all required parameters as defined in the tool schema." + ) + + compact = AskUserQuestionTool().render_tool_result_message(long_error, is_error=True) + + assert compact == "ask_user_question validation failed." + assert "tech_stack_nodejs" not in compact + assert "not of type" not in compact + class TestAskUserQuestionToolExecute: @pytest.mark.asyncio diff --git a/tests/ui/test_renderer_helpers.py b/tests/ui/test_renderer_helpers.py index 074e8166..7d07026f 100644 --- a/tests/ui/test_renderer_helpers.py +++ b/tests/ui/test_renderer_helpers.py @@ -376,6 +376,31 @@ def test_replay_history_summarizes_legacy_complete_step_error_without_metadata(s assert "'conclusion' is a required property" not in output assert "schema summary" not in output + def test_replay_history_summarizes_legacy_ask_user_question_error_without_metadata(self): + renderer = Renderer(make_console(), ToolRegistry(), status_callback=lambda: "ready") + long_error = ( + "Invalid input for tool 'ask_user_question': " + "[{'id': 'tech_stack_nodejs', 'label': 'Node.js'}] is not of type 'object'. " + "Please provide all required parameters as defined in the tool schema." + ) + messages = [ + Message( + role="assistant", + content=[ToolUseBlock(id="tu_bad", name="ask_user_question", input={})], + ), + Message( + role="user", + content=[ToolResultBlock(tool_use_id="tu_bad", content=long_error, is_error=True)], + ), + ] + + renderer.replay_history(messages) + + output = renderer.console.file.getvalue() + assert "ask_user_question validation failed." in output + assert "tech_stack_nodejs" not in output + assert "not of type" not in output + def test_render_progress_groups_include_resource_rows(self): renderer = make_renderer()