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.
Files changed (140) hide show
  1. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/PKG-INFO +1 -1
  2. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/pyproject.toml +1 -1
  3. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/__init__.py +1 -1
  4. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/agent_adapter.py +102 -6
  5. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/sdk_lifecycle.py +4 -3
  6. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/PKG-INFO +1 -1
  7. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_per_capability_llm_config_model_routing.py +143 -0
  8. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_release_tag_version_guardrail.py +2 -2
  9. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/README.md +0 -0
  10. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/setup.cfg +0 -0
  11. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/__init__.py +0 -0
  12. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/agently_backend.py +0 -0
  13. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/triggerflow_workflow_engine.py +0 -0
  14. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/adapters/workflow_engine.py +0 -0
  15. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/config.py +0 -0
  16. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/errors.py +0 -0
  17. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/guards.py +0 -0
  18. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_protocol.py +0 -0
  19. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/__init__.py +0 -0
  20. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/approvals_profiles.py +0 -0
  21. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/evidence_hooks.py +0 -0
  22. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/history.py +0 -0
  23. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/invoke_capability.py +0 -0
  24. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/resume.py +0 -0
  25. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/system_prompt.py +0 -0
  26. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/turn_delta.py +0 -0
  27. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/logging_utils.py +0 -0
  28. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/manifest.py +0 -0
  29. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/output_validator.py +0 -0
  30. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/__init__.py +0 -0
  31. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/agent.py +0 -0
  32. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/capability.py +0 -0
  33. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/chat_backend.py +0 -0
  34. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/context.py +0 -0
  35. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/protocol/workflow.py +0 -0
  36. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/registry.py +0 -0
  37. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/reporting/__init__.py +0 -0
  38. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/reporting/node_report.py +0 -0
  39. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/runtime.py +0 -0
  40. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/runtime_ui_events_mixin.py +0 -0
  41. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/service_facade.py +0 -0
  42. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/services.py +0 -0
  43. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/structured_output.py +0 -0
  44. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/structured_stream.py +0 -0
  45. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/types.py +0 -0
  46. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/__init__.py +0 -0
  47. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/projector.py +0 -0
  48. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/session.py +0 -0
  49. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/store.py +0 -0
  50. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/transport.py +0 -0
  51. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/v1.py +0 -0
  52. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/upstream_compat.py +0 -0
  53. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/utils/__init__.py +0 -0
  54. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/utils/usage.py +0 -0
  55. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime/workflow_runtime.py +0 -0
  56. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/SOURCES.txt +0 -0
  57. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/dependency_links.txt +0 -0
  58. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/requires.txt +0 -0
  59. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/top_level.txt +0 -0
  60. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_agently_backend.py +0 -0
  61. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_agently_backend_replay.py +0 -0
  62. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_backend_mode.py +0 -0
  63. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_bilingual_docs_surface.py +0 -0
  64. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_bridge_artifacts_passthrough.py +0 -0
  65. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_bridge_register_tool_public_api.py +0 -0
  66. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_coding_agent_examples_atomic.py +0 -0
  67. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_coding_agent_examples_recipes.py +0 -0
  68. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_config_glue.py +0 -0
  69. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_dependency_pins.py +0 -0
  70. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_docs_pinned_dependency_versions.py +0 -0
  71. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_docs_scheme2_no_skilladapter_residue.py +0 -0
  72. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_error_observability.py +0 -0
  73. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_alignment_fixes_l1.py +0 -0
  74. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_no_agent_sdk_imports.py +0 -0
  75. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_real_evidence_strict_integration.py +0 -0
  76. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_real_integration.py +0 -0
  77. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_smoke.py +0 -0
  78. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_offline.py +0 -0
  79. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_real_fallback.py +0 -0
  80. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_real_integration.py +0 -0
  81. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_ui_contract.py +0 -0
  82. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_examples_workflow_skills_first_smoke.py +0 -0
  83. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_guards.py +0 -0
  84. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_approvals_profiles.py +0 -0
  85. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_history_assembler.py +0 -0
  86. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_invoke_capability.py +0 -0
  87. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_invoke_capability_shared_runtime.py +0 -0
  88. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_resume_helper.py +0 -0
  89. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_system_prompt_evidence.py +0 -0
  90. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_host_toolkit_turn_delta.py +0 -0
  91. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_integration_agently_requester_smoke.py +0 -0
  92. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_integration_approval_event_shape.py +0 -0
  93. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_integration_sources_redis_pgsql_smoke.py +0 -0
  94. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_loop.py +0 -0
  95. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_node_report_builder.py +0 -0
  96. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_node_report_contract_v1.py +0 -0
  97. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_node_report_engine_identity_contract.py +0 -0
  98. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_offline_backend_injection_evidence.py +0 -0
  99. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_preflight_gate.py +0 -0
  100. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_project_identity_naming_matrix.py +0 -0
  101. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_public_api_exports.py +0 -0
  102. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_public_repo_hygiene.py +0 -0
  103. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_qa_agent.py +0 -0
  104. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_registry.py +0 -0
  105. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_replay_tool_calls_alignment.py +0 -0
  106. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_repo_hygiene_no_tracked_env_or_pyc.py +0 -0
  107. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_repo_no_deep_imports_in_user_facing_docs.py +0 -0
  108. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_review_followups_module_contracts.py +0 -0
  109. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_bridge_fake_backend.py +0 -0
  110. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_concurrency.py +0 -0
  111. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_engine.py +0 -0
  112. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_hitl_host_protocol.py +0 -0
  113. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_hooks_and_schema_gate.py +0 -0
  114. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_initial_history_and_meta.py +0 -0
  115. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_manifest.py +0 -0
  116. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_result_prompt_hardening.py +0 -0
  117. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_run_stream_semantics_v1.py +0 -0
  118. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_service_facade_rpc_mapping.py +0 -0
  119. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_service_session_bridge.py +0 -0
  120. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_status_mapping.py +0 -0
  121. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_structured_output_bridge.py +0 -0
  122. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_runtime_structured_stream.py +0 -0
  123. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_sandbox_permissions_passthrough_contract.py +0 -0
  124. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_services_call_callback.py +0 -0
  125. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_services_map_node_status.py +0 -0
  126. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_skills_conformance_smoke.py +0 -0
  127. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_adapter_tool_registration.py +0 -0
  128. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_chat_backend_protocol_contract.py +0 -0
  129. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_chat_sse_usage_contract.py +0 -0
  130. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_compat_spaces_schema.py +0 -0
  131. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_prompt_profile_contract.py +0 -0
  132. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_runtime_client_server_contract.py +0 -0
  133. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_sandbox_profile_precedence_contract.py +0 -0
  134. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_skills_bundles_contract.py +0 -0
  135. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_tool_descriptor_compat.py +0 -0
  136. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_upstream_verification.py +0 -0
  137. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_versioning_strategy_guard.py +0 -0
  138. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_wal_backend_injection_contract.py +0 -0
  139. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_wal_locator_resolution_contract.py +0 -0
  140. {capability_runtime-0.1.3.post1 → capability_runtime-0.1.4}/tests/test_workflow_host_runtime_surface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capability-runtime
3
- Version: 0.1.3.post1
3
+ Version: 0.1.4
4
4
  Summary: Bridge/glue layer that composes Agently (LLM/TriggerFlow) with skills-runtime-sdk (skills/tools/WAL/events).
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "capability-runtime"
3
- version = "0.1.3.post1"
3
+ version = "0.1.4"
4
4
  description = "Bridge/glue layer that composes Agently (LLM/TriggerFlow) with skills-runtime-sdk (skills/tools/WAL/events)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,7 +1,7 @@
1
1
  """capability-runtime:统一 Runtime 入口(能力协议 + 执行 + 报告)。"""
2
2
  from __future__ import annotations
3
3
 
4
- __version__ = "0.1.3.post1"
4
+ __version__ = "0.1.4"
5
5
 
6
6
  # === 统一入口 ===
7
7
  from .config import CustomTool, RuntimeConfig
@@ -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 = json.dumps(messages, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
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 与字符串 content;
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
@@ -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 做浅拷贝转发,避免下游 backend 修改 host 输入。
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]] = [dict(item) for item in messages]
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=[dict(item) for item in self._messages],
620
+ value=deepcopy(self._messages),
620
621
  dataclasses_context="precomposed_messages_dataclasses_replace",
621
622
  clone_context="precomposed_messages_override",
622
623
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capability-runtime
3
- Version: 0.1.3.post1
3
+ Version: 0.1.4
4
4
  Summary: Bridge/glue layer that composes Agently (LLM/TriggerFlow) with skills-runtime-sdk (skills/tools/WAL/events).
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -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.3.post1",
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.3.post1"
29
+ assert tag_version == pyproject_version == module_version == "0.1.4"
30
30
 
31
31
 
32
32
  def test_release_guard_rejects_mismatch() -> None: