capability-runtime 0.1.3.post1__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/PKG-INFO +1 -1
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/pyproject.toml +1 -1
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/__init__.py +1 -1
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/agent_adapter.py +102 -6
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/sdk_lifecycle.py +4 -3
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/PKG-INFO +1 -1
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_per_capability_llm_config_model_routing.py +143 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_release_tag_version_guardrail.py +2 -2
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/README.md +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/setup.cfg +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/__init__.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/agently_backend.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/triggerflow_workflow_engine.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/workflow_engine.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/config.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/errors.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/guards.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_protocol.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/__init__.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/approvals_profiles.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/evidence_hooks.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/history.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/invoke_capability.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/resume.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/system_prompt.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/turn_delta.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/logging_utils.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/manifest.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/output_validator.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/__init__.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/agent.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/capability.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/chat_backend.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/context.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/workflow.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/registry.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/reporting/__init__.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/reporting/node_report.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/runtime.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/runtime_ui_events_mixin.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/service_facade.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/services.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/structured_output.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/structured_stream.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/types.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/__init__.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/projector.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/session.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/store.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/transport.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/v1.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/upstream_compat.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/utils/__init__.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/utils/usage.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/workflow_runtime.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/SOURCES.txt +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/dependency_links.txt +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/requires.txt +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/top_level.txt +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_agently_backend.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_agently_backend_replay.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_backend_mode.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_bilingual_docs_surface.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_bridge_artifacts_passthrough.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_bridge_register_tool_public_api.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_coding_agent_examples_atomic.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_coding_agent_examples_recipes.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_config_glue.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_dependency_pins.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_docs_pinned_dependency_versions.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_docs_scheme2_no_skilladapter_residue.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_error_observability.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_alignment_fixes_l1.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_no_agent_sdk_imports.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_real_evidence_strict_integration.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_real_integration.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_smoke.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_offline.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_real_fallback.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_real_integration.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_ui_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_workflow_skills_first_smoke.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_guards.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_approvals_profiles.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_history_assembler.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_invoke_capability.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_invoke_capability_shared_runtime.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_resume_helper.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_system_prompt_evidence.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_turn_delta.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_integration_agently_requester_smoke.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_integration_approval_event_shape.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_integration_sources_redis_pgsql_smoke.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_loop.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_node_report_builder.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_node_report_contract_v1.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_node_report_engine_identity_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_offline_backend_injection_evidence.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_preflight_gate.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_project_identity_naming_matrix.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_public_api_exports.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_public_repo_hygiene.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_qa_agent.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_registry.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_replay_tool_calls_alignment.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_repo_hygiene_no_tracked_env_or_pyc.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_repo_no_deep_imports_in_user_facing_docs.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_review_followups_module_contracts.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_bridge_fake_backend.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_concurrency.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_engine.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_hitl_host_protocol.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_hooks_and_schema_gate.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_initial_history_and_meta.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_manifest.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_result_prompt_hardening.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_run_stream_semantics_v1.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_service_facade_rpc_mapping.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_service_session_bridge.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_status_mapping.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_structured_output_bridge.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_structured_stream.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_sandbox_permissions_passthrough_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_services_call_callback.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_services_map_node_status.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_skills_conformance_smoke.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_adapter_tool_registration.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_chat_backend_protocol_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_chat_sse_usage_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_compat_spaces_schema.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_prompt_profile_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_runtime_client_server_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_sandbox_profile_precedence_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_skills_bundles_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_tool_descriptor_compat.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_verification.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_versioning_strategy_guard.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_wal_backend_injection_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_wal_locator_resolution_contract.py +0 -0
- {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_workflow_host_runtime_surface.py +0 -0
|
@@ -37,6 +37,7 @@ _RUNTIME_PROMPT_KEY = "_runtime_prompt"
|
|
|
37
37
|
_PROMPT_HASH_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
38
38
|
_ALLOWED_PROMPT_ROLES = {"system", "developer", "user", "assistant", "tool"}
|
|
39
39
|
_ALLOWED_PROMPT_PROFILES = {"default_agent", "generation_direct", "structured_transform"}
|
|
40
|
+
_ALLOWED_IMAGE_DETAILS = {"auto", "low", "high"}
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
class _InvalidPromptMessages(ValueError):
|
|
@@ -578,6 +579,10 @@ class AgentAdapter:
|
|
|
578
579
|
if messages is not None:
|
|
579
580
|
evidence["prompt_messages_count"] = len(messages)
|
|
580
581
|
evidence["prompt_message_roles"] = [str(item["role"]) for item in messages]
|
|
582
|
+
modalities, content_part_counts, media_count = _summarize_precomposed_message_content(messages)
|
|
583
|
+
evidence["prompt_modalities"] = modalities
|
|
584
|
+
evidence["prompt_content_part_counts"] = content_part_counts
|
|
585
|
+
evidence["prompt_media_count"] = media_count
|
|
581
586
|
return evidence
|
|
582
587
|
|
|
583
588
|
def _strip_runtime_prompt(self, input: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -736,18 +741,27 @@ def _hash_text(text: str) -> str:
|
|
|
736
741
|
def _hash_messages(messages: List[Dict[str, Any]]) -> str:
|
|
737
742
|
"""对 provider messages 做稳定 JSON canonicalization 后生成摘要。"""
|
|
738
743
|
|
|
739
|
-
canonical =
|
|
744
|
+
canonical = _canonicalize_messages(messages)
|
|
740
745
|
return _hash_text(canonical)
|
|
741
746
|
|
|
742
747
|
|
|
748
|
+
def _canonicalize_messages(messages: List[Dict[str, Any]]) -> str:
|
|
749
|
+
"""对 provider messages 做稳定 JSON canonicalization。"""
|
|
750
|
+
|
|
751
|
+
try:
|
|
752
|
+
return json.dumps(messages, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
753
|
+
except (TypeError, ValueError) as exc:
|
|
754
|
+
raise _InvalidPromptMessages("_runtime_prompt.messages must be JSON canonicalizable") from exc
|
|
755
|
+
|
|
756
|
+
|
|
743
757
|
def _validate_precomposed_messages(raw: Any) -> List[Dict[str, Any]]:
|
|
744
758
|
"""
|
|
745
759
|
校验 host 提供的最终 provider messages。
|
|
746
760
|
|
|
747
761
|
约束:
|
|
748
762
|
- 只接受非空 list[dict];
|
|
749
|
-
- 每条消息必须有合法 role
|
|
750
|
-
-
|
|
763
|
+
- 每条消息必须有合法 role 与字符串或稳定 content parts;
|
|
764
|
+
- 返回深拷贝,避免后续执行链路修改调用方输入。
|
|
751
765
|
"""
|
|
752
766
|
|
|
753
767
|
if not isinstance(raw, list):
|
|
@@ -763,10 +777,92 @@ def _validate_precomposed_messages(raw: Any) -> List[Dict[str, Any]]:
|
|
|
763
777
|
content = item.get("content")
|
|
764
778
|
if not isinstance(role, str) or role not in _ALLOWED_PROMPT_ROLES:
|
|
765
779
|
raise _InvalidPromptMessages(f"_runtime_prompt.messages[{idx}].role is invalid")
|
|
766
|
-
if not isinstance(content, str):
|
|
767
|
-
raise _InvalidPromptMessages(f"_runtime_prompt.messages[{idx}].content must be a string")
|
|
768
780
|
copied = dict(item)
|
|
769
781
|
copied["role"] = role
|
|
770
|
-
copied["content"] = content
|
|
782
|
+
copied["content"] = _validate_precomposed_message_content(content, message_index=idx)
|
|
771
783
|
out.append(copied)
|
|
784
|
+
return json.loads(_canonicalize_messages(out))
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _validate_precomposed_message_content(content: Any, *, message_index: int) -> Union[str, List[Dict[str, Any]]]:
|
|
788
|
+
"""校验单条 precomposed message 的 content 字段。"""
|
|
789
|
+
|
|
790
|
+
if isinstance(content, str):
|
|
791
|
+
return content
|
|
792
|
+
if not isinstance(content, list):
|
|
793
|
+
raise _InvalidPromptMessages(
|
|
794
|
+
f"_runtime_prompt.messages[{message_index}].content must be a string or content part list"
|
|
795
|
+
)
|
|
796
|
+
if not content:
|
|
797
|
+
raise _InvalidPromptMessages(f"_runtime_prompt.messages[{message_index}].content must not be an empty list")
|
|
798
|
+
|
|
799
|
+
out: List[Dict[str, Any]] = []
|
|
800
|
+
for part_index, part in enumerate(content):
|
|
801
|
+
out.append(_validate_precomposed_content_part(part, message_index=message_index, part_index=part_index))
|
|
772
802
|
return out
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _validate_precomposed_content_part(part: Any, *, message_index: int, part_index: int) -> Dict[str, Any]:
|
|
806
|
+
"""校验 v1 稳定支持的 OpenAI-compatible content part。"""
|
|
807
|
+
|
|
808
|
+
path = f"_runtime_prompt.messages[{message_index}].content[{part_index}]"
|
|
809
|
+
if not isinstance(part, dict):
|
|
810
|
+
raise _InvalidPromptMessages(f"{path} must be a dict")
|
|
811
|
+
part_type = part.get("type")
|
|
812
|
+
if not isinstance(part_type, str) or not part_type:
|
|
813
|
+
raise _InvalidPromptMessages(f"{path}.type is required")
|
|
814
|
+
if part_type == "text":
|
|
815
|
+
if set(part.keys()) != {"type", "text"}:
|
|
816
|
+
raise _InvalidPromptMessages(f"{path} has unsupported text part fields")
|
|
817
|
+
text = part.get("text")
|
|
818
|
+
if not isinstance(text, str):
|
|
819
|
+
raise _InvalidPromptMessages(f"{path}.text must be a string")
|
|
820
|
+
return {"type": "text", "text": text}
|
|
821
|
+
if part_type == "image_url":
|
|
822
|
+
if set(part.keys()) != {"type", "image_url"}:
|
|
823
|
+
raise _InvalidPromptMessages(f"{path} has unsupported image_url part fields")
|
|
824
|
+
image_url = part.get("image_url")
|
|
825
|
+
if not isinstance(image_url, dict):
|
|
826
|
+
raise _InvalidPromptMessages(f"{path}.image_url must be a dict")
|
|
827
|
+
allowed_image_keys = {"url", "detail"}
|
|
828
|
+
if not set(image_url.keys()).issubset(allowed_image_keys):
|
|
829
|
+
raise _InvalidPromptMessages(f"{path}.image_url has unsupported fields")
|
|
830
|
+
url = image_url.get("url")
|
|
831
|
+
if not isinstance(url, str) or not url.strip():
|
|
832
|
+
raise _InvalidPromptMessages(f"{path}.image_url.url must be a non-empty string")
|
|
833
|
+
copied_image_url: Dict[str, Any] = {"url": url}
|
|
834
|
+
if "detail" in image_url:
|
|
835
|
+
detail = image_url.get("detail")
|
|
836
|
+
if detail not in _ALLOWED_IMAGE_DETAILS:
|
|
837
|
+
raise _InvalidPromptMessages(f"{path}.image_url.detail is invalid")
|
|
838
|
+
copied_image_url["detail"] = detail
|
|
839
|
+
return {"type": "image_url", "image_url": copied_image_url}
|
|
840
|
+
raise _InvalidPromptMessages(f"{path}.type is unsupported")
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _summarize_precomposed_message_content(
|
|
844
|
+
messages: List[Dict[str, Any]],
|
|
845
|
+
) -> tuple[List[str], List[int], int]:
|
|
846
|
+
"""生成 precomposed messages 的最小披露多模态摘要。"""
|
|
847
|
+
|
|
848
|
+
modalities: set[str] = set()
|
|
849
|
+
content_part_counts: List[int] = []
|
|
850
|
+
media_count = 0
|
|
851
|
+
for message in messages:
|
|
852
|
+
content = message.get("content")
|
|
853
|
+
if isinstance(content, str):
|
|
854
|
+
modalities.add("text")
|
|
855
|
+
content_part_counts.append(0)
|
|
856
|
+
continue
|
|
857
|
+
if isinstance(content, list):
|
|
858
|
+
content_part_counts.append(len(content))
|
|
859
|
+
for part in content:
|
|
860
|
+
part_type = part.get("type") if isinstance(part, dict) else None
|
|
861
|
+
if part_type == "text":
|
|
862
|
+
modalities.add("text")
|
|
863
|
+
elif part_type == "image_url":
|
|
864
|
+
modalities.add("image")
|
|
865
|
+
media_count += 1
|
|
866
|
+
continue
|
|
867
|
+
content_part_counts.append(0)
|
|
868
|
+
return sorted(modalities), content_part_counts, media_count
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/sdk_lifecycle.py
RENAMED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import inspect
|
|
7
7
|
import uuid
|
|
8
|
+
from copy import deepcopy
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
from pathlib import Path
|
|
@@ -598,12 +599,12 @@ class _PrecomposedMessagesBackend:
|
|
|
598
599
|
|
|
599
600
|
说明:
|
|
600
601
|
- 该包装不绕过 SDK Agent / WAL / events,只在 provider request 出口替换 messages;
|
|
601
|
-
- messages
|
|
602
|
+
- messages 做嵌套结构副本转发,避免 host/backend 后续修改共享可变对象。
|
|
602
603
|
"""
|
|
603
604
|
|
|
604
605
|
def __init__(self, *, backend: ChatBackendProtocol, messages: List[Dict[str, Any]]) -> None:
|
|
605
606
|
self._backend: ChatBackendProtocol = backend
|
|
606
|
-
self._messages: List[Dict[str, Any]] =
|
|
607
|
+
self._messages: List[Dict[str, Any]] = deepcopy(messages)
|
|
607
608
|
|
|
608
609
|
async def stream_chat(self, request: ChatRequest) -> AsyncGenerator[ChatStreamEvent, None]:
|
|
609
610
|
"""
|
|
@@ -616,7 +617,7 @@ class _PrecomposedMessagesBackend:
|
|
|
616
617
|
forwarded = _clone_request_with_field_update(
|
|
617
618
|
request,
|
|
618
619
|
field_name="messages",
|
|
619
|
-
value=
|
|
620
|
+
value=deepcopy(self._messages),
|
|
620
621
|
dataclasses_context="precomposed_messages_dataclasses_replace",
|
|
621
622
|
clone_context="precomposed_messages_override",
|
|
622
623
|
)
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
- 当 llm_config 缺失/不含 model 时:不得做覆写(保持 runtime 默认行为)。
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import copy
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any, AsyncIterator, Dict, List, Optional
|
|
13
14
|
|
|
@@ -20,6 +21,7 @@ from skills_runtime.tools.protocol import ToolSpec
|
|
|
20
21
|
from capability_runtime import AgentSpec, CapabilityKind, CapabilitySpec, CustomTool, Runtime, RuntimeConfig
|
|
21
22
|
from capability_runtime.sdk_lifecycle import (
|
|
22
23
|
_ModelOverrideBackend,
|
|
24
|
+
_PrecomposedMessagesBackend,
|
|
23
25
|
_ResponseFormatOverrideBackend,
|
|
24
26
|
_ToolChoiceOverrideBackend,
|
|
25
27
|
_UsageTapBackend,
|
|
@@ -56,6 +58,28 @@ class _RecordingBackend:
|
|
|
56
58
|
yield ChatStreamEvent(type="completed")
|
|
57
59
|
|
|
58
60
|
|
|
61
|
+
class _MutatingMessagesBackend:
|
|
62
|
+
"""
|
|
63
|
+
测试用 ChatBackend:记录收到的 request.messages,然后故意篡改嵌套字段。
|
|
64
|
+
|
|
65
|
+
该 backend 用于证明 `_PrecomposedMessagesBackend` 每次转发都生成独立嵌套副本,
|
|
66
|
+
下游 backend 的原地修改不会污染 lifecycle 缓存或下一次 provider request。
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
self.messages_before_mutation: List[List[Dict[str, Any]]] = []
|
|
71
|
+
|
|
72
|
+
async def stream_chat(self, request: ChatRequest) -> AsyncIterator[ChatStreamEvent]:
|
|
73
|
+
messages = getattr(request, "messages", None)
|
|
74
|
+
if isinstance(messages, list):
|
|
75
|
+
self.messages_before_mutation.append(copy.deepcopy(messages))
|
|
76
|
+
messages[1]["content"][1]["image_url"]["url"] = "https://mutated.example/image.png"
|
|
77
|
+
messages[2]["tool_calls"][0]["function"]["arguments"] = '{"mutated": true}'
|
|
78
|
+
messages[3]["metadata"]["score"] = 0
|
|
79
|
+
yield ChatStreamEvent(type="text_delta", text="ok")
|
|
80
|
+
yield ChatStreamEvent(type="completed")
|
|
81
|
+
|
|
82
|
+
|
|
59
83
|
class _BrokenCloneRequest:
|
|
60
84
|
"""模拟 request 暴露 copy/model_copy,但内部复制总是失败。"""
|
|
61
85
|
|
|
@@ -114,6 +138,45 @@ def _agent_spec_with_prompt(
|
|
|
114
138
|
)
|
|
115
139
|
|
|
116
140
|
|
|
141
|
+
def _multimodal_messages() -> List[Dict[str, Any]]:
|
|
142
|
+
"""
|
|
143
|
+
构造覆盖多模态 content parts 与 assistant/tool extra fields 的 provider messages。
|
|
144
|
+
|
|
145
|
+
返回:
|
|
146
|
+
- 可直接作为 precomposed_messages 使用的 JSON-serializable message 列表。
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
return [
|
|
150
|
+
{"role": "system", "content": "You inspect multimodal context."},
|
|
151
|
+
{
|
|
152
|
+
"role": "user",
|
|
153
|
+
"content": [
|
|
154
|
+
{"type": "text", "text": "Compare the images."},
|
|
155
|
+
{"type": "image_url", "image_url": {"url": "https://example.test/a.png", "detail": "high"}},
|
|
156
|
+
{"type": "image_url", "image_url": {"url": "https://example.test/b.png"}},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"role": "assistant",
|
|
161
|
+
"content": "I need the image metadata.",
|
|
162
|
+
"tool_calls": [
|
|
163
|
+
{
|
|
164
|
+
"id": "call_1",
|
|
165
|
+
"type": "function",
|
|
166
|
+
"function": {"name": "inspect_image", "arguments": '{"image": "a"}'},
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"role": "tool",
|
|
172
|
+
"tool_call_id": "call_1",
|
|
173
|
+
"name": "inspect_image",
|
|
174
|
+
"content": '{"width": 1280}',
|
|
175
|
+
"metadata": {"score": 1, "labels": ["diagram"]},
|
|
176
|
+
},
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
|
|
117
180
|
@pytest.mark.asyncio
|
|
118
181
|
async def test_agent_spec_llm_config_model_overrides_backend_request_model(tmp_path: Path) -> None:
|
|
119
182
|
backend = _RecordingBackend()
|
|
@@ -364,6 +427,86 @@ async def test_precomposed_messages_override_final_backend_request_messages(tmp_
|
|
|
364
427
|
assert "Write one clean sentence" not in out.node_report.model_dump_json()
|
|
365
428
|
|
|
366
429
|
|
|
430
|
+
@pytest.mark.asyncio
|
|
431
|
+
async def test_precomposed_multimodal_messages_override_final_backend_request_messages(tmp_path: Path) -> None:
|
|
432
|
+
"""
|
|
433
|
+
Multimodal Boundary v1:多模态 messages 与 assistant/tool extra fields 必须等价到达 backend。
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
backend = _RecordingBackend()
|
|
437
|
+
rt = Runtime(
|
|
438
|
+
RuntimeConfig(
|
|
439
|
+
mode="sdk_native",
|
|
440
|
+
workspace_root=tmp_path,
|
|
441
|
+
preflight_mode="off",
|
|
442
|
+
sdk_backend=backend,
|
|
443
|
+
)
|
|
444
|
+
)
|
|
445
|
+
rt.register(
|
|
446
|
+
_agent_spec_with_prompt(
|
|
447
|
+
agent_id="agent.multimodal.messages",
|
|
448
|
+
prompt_render_mode="precomposed_messages",
|
|
449
|
+
prompt_profile="generation_direct",
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
messages = _multimodal_messages()
|
|
453
|
+
|
|
454
|
+
out = await rt.run(
|
|
455
|
+
"agent.multimodal.messages",
|
|
456
|
+
input={"_runtime_prompt": {"messages": messages}},
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
assert out.status.value == "success"
|
|
460
|
+
assert backend.messages
|
|
461
|
+
assert messages in backend.messages
|
|
462
|
+
observed = backend.messages[-1]
|
|
463
|
+
assert observed[1]["content"] == messages[1]["content"]
|
|
464
|
+
assert observed[2]["tool_calls"] == messages[2]["tool_calls"]
|
|
465
|
+
assert observed[3]["tool_call_id"] == "call_1"
|
|
466
|
+
assert observed[3]["name"] == "inspect_image"
|
|
467
|
+
assert observed[3]["metadata"] == {"score": 1, "labels": ["diagram"]}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@pytest.mark.asyncio
|
|
471
|
+
async def test_precomposed_multimodal_messages_host_mutation_does_not_affect_request() -> None:
|
|
472
|
+
"""
|
|
473
|
+
Multimodal Boundary v1:host 后续修改 nested content parts 不得污染 provider request。
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
backend = _RecordingBackend()
|
|
477
|
+
messages = _multimodal_messages()
|
|
478
|
+
expected = copy.deepcopy(messages)
|
|
479
|
+
wrapped = _PrecomposedMessagesBackend(backend=backend, messages=messages)
|
|
480
|
+
|
|
481
|
+
messages[1]["content"][1]["image_url"]["url"] = "https://host-mutated.example/a.png"
|
|
482
|
+
messages[2]["tool_calls"][0]["function"]["arguments"] = '{"image": "mutated"}'
|
|
483
|
+
messages[3]["metadata"]["labels"].append("host-mutated")
|
|
484
|
+
|
|
485
|
+
async for _ in wrapped.stream_chat(ChatRequest(model="test-model", messages=[])):
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
assert backend.messages == [expected]
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@pytest.mark.asyncio
|
|
492
|
+
async def test_precomposed_multimodal_messages_backend_mutation_does_not_pollute_next_request() -> None:
|
|
493
|
+
"""
|
|
494
|
+
Multimodal Boundary v1:backend 原地修改 request.messages 不得污染 lifecycle 缓存或下一次请求。
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
backend = _MutatingMessagesBackend()
|
|
498
|
+
messages = _multimodal_messages()
|
|
499
|
+
expected = copy.deepcopy(messages)
|
|
500
|
+
wrapped = _PrecomposedMessagesBackend(backend=backend, messages=messages)
|
|
501
|
+
|
|
502
|
+
async for _ in wrapped.stream_chat(ChatRequest(model="test-model", messages=[])):
|
|
503
|
+
pass
|
|
504
|
+
async for _ in wrapped.stream_chat(ChatRequest(model="test-model", messages=[])):
|
|
505
|
+
pass
|
|
506
|
+
|
|
507
|
+
assert backend.messages_before_mutation == [expected, expected]
|
|
508
|
+
|
|
509
|
+
|
|
367
510
|
@pytest.mark.asyncio
|
|
368
511
|
async def test_prompt_profile_generation_direct_reaches_sdk_prompt_config_and_hides_tools(
|
|
369
512
|
tmp_path: Path,
|
|
@@ -22,11 +22,11 @@ def _load_release_guard_module():
|
|
|
22
22
|
def test_release_guard_accepts_current_tag() -> None:
|
|
23
23
|
module = _load_release_guard_module()
|
|
24
24
|
tag_version, pyproject_version, module_version = module.validate_versions(
|
|
25
|
-
release_tag="v0.1.
|
|
25
|
+
release_tag="v0.1.4",
|
|
26
26
|
pyproject_path=_REPO_ROOT / "pyproject.toml",
|
|
27
27
|
init_path=_REPO_ROOT / "src" / "capability_runtime" / "__init__.py",
|
|
28
28
|
)
|
|
29
|
-
assert tag_version == pyproject_version == module_version == "0.1.
|
|
29
|
+
assert tag_version == pyproject_version == module_version == "0.1.4"
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def test_release_guard_rejects_mismatch() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/config.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/errors.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/guards.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/logging_utils.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/manifest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/agent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/runtime.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/service_facade.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/services.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/utils/__init__.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/utils/usage.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_agently_backend_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_bilingual_docs_surface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_error_observability.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_real_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_resume_helper.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_turn_delta.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_node_report_builder.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_node_report_contract_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_public_api_exports.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_public_repo_hygiene.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_concurrency.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_hitl_host_protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_status_mapping.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_structured_stream.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_services_call_callback.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_services_map_node_status.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_skills_conformance_smoke.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_verification.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_versioning_strategy_guard.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|