capability-runtime 0.1.3__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 → capability_runtime-0.1.4}/PKG-INFO +1 -1
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/pyproject.toml +1 -1
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/__init__.py +1 -1
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/adapters/agent_adapter.py +102 -6
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/sdk_lifecycle.py +27 -5
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/PKG-INFO +1 -1
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_agently_backend.py +152 -1
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_per_capability_llm_config_model_routing.py +143 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_release_tag_version_guardrail.py +2 -2
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/README.md +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/setup.cfg +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/adapters/__init__.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/adapters/agently_backend.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/adapters/triggerflow_workflow_engine.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/adapters/workflow_engine.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/config.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/errors.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/guards.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_protocol.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/__init__.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/approvals_profiles.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/evidence_hooks.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/history.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/invoke_capability.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/resume.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/system_prompt.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/turn_delta.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/logging_utils.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/manifest.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/output_validator.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/__init__.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/agent.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/capability.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/chat_backend.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/context.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/workflow.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/registry.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/reporting/__init__.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/reporting/node_report.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/runtime.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/runtime_ui_events_mixin.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/service_facade.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/services.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/structured_output.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/structured_stream.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/types.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/__init__.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/projector.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/session.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/store.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/transport.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/v1.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/upstream_compat.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/utils/__init__.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/utils/usage.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/workflow_runtime.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/SOURCES.txt +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/dependency_links.txt +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/requires.txt +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/top_level.txt +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_agently_backend_replay.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_backend_mode.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_bilingual_docs_surface.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_bridge_artifacts_passthrough.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_bridge_register_tool_public_api.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_coding_agent_examples_atomic.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_coding_agent_examples_recipes.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_config_glue.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_dependency_pins.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_docs_pinned_dependency_versions.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_docs_scheme2_no_skilladapter_residue.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_error_observability.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_alignment_fixes_l1.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_no_agent_sdk_imports.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_real_evidence_strict_integration.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_real_integration.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_smoke.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_offline.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_real_fallback.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_real_integration.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_ui_events_showcase_ui_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_workflow_skills_first_smoke.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_guards.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_approvals_profiles.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_history_assembler.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_invoke_capability.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_invoke_capability_shared_runtime.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_resume_helper.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_system_prompt_evidence.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_turn_delta.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_integration_agently_requester_smoke.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_integration_approval_event_shape.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_integration_sources_redis_pgsql_smoke.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_loop.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_node_report_builder.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_node_report_contract_v1.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_node_report_engine_identity_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_offline_backend_injection_evidence.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_preflight_gate.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_project_identity_naming_matrix.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_public_api_exports.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_public_repo_hygiene.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_qa_agent.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_registry.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_replay_tool_calls_alignment.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_repo_hygiene_no_tracked_env_or_pyc.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_repo_no_deep_imports_in_user_facing_docs.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_review_followups_module_contracts.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_bridge_fake_backend.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_concurrency.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_engine.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_hitl_host_protocol.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_hooks_and_schema_gate.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_initial_history_and_meta.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_manifest.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_result_prompt_hardening.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_run_stream_semantics_v1.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_service_facade_rpc_mapping.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_service_session_bridge.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_status_mapping.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_structured_output_bridge.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_structured_stream.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_sandbox_permissions_passthrough_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_services_call_callback.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_services_map_node_status.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_skills_conformance_smoke.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_adapter_tool_registration.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_chat_backend_protocol_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_chat_sse_usage_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_compat_spaces_schema.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_prompt_profile_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_runtime_client_server_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_sandbox_profile_precedence_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_skills_bundles_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_tool_descriptor_compat.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_verification.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_versioning_strategy_guard.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_wal_backend_injection_contract.py +0 -0
- {capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_wal_locator_resolution_contract.py +0 -0
- {capability_runtime-0.1.3 → 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 → 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
|
)
|
|
@@ -738,6 +739,9 @@ def _now_rfc3339() -> str:
|
|
|
738
739
|
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
739
740
|
|
|
740
741
|
|
|
742
|
+
_USAGE_PROVIDER_PLACEHOLDERS = {"openai", "openai-compatible"}
|
|
743
|
+
|
|
744
|
+
|
|
741
745
|
def _merge_supplemental_usage_metadata_event(*, ev: AgentEvent, supplemental_payloads: List[Dict[str, Any]]) -> AgentEvent:
|
|
742
746
|
"""
|
|
743
747
|
把 sink payload 作为 supplemental metadata patch 合并到上游 `llm_usage` 事件。
|
|
@@ -747,7 +751,8 @@ def _merge_supplemental_usage_metadata_event(*, ev: AgentEvent, supplemental_pay
|
|
|
747
751
|
- supplemental_payloads:`_caprt_usage_sink` 收集到的 provider/gateway usage payload 列表
|
|
748
752
|
|
|
749
753
|
返回:
|
|
750
|
-
- 新 AgentEvent;仅在上游 metadata 缺失时补 `model/request_id/provider`,不写入 token
|
|
754
|
+
- 新 AgentEvent;仅在上游 metadata 缺失时补 `model/request_id/provider`,不写入 token 字段;
|
|
755
|
+
`provider` 可在上游为 OpenAI-compatible 占位值时被 effective provider 受控覆盖。
|
|
751
756
|
"""
|
|
752
757
|
|
|
753
758
|
upstream_payload = dict(ev.payload) if isinstance(ev.payload, dict) else {}
|
|
@@ -755,13 +760,30 @@ def _merge_supplemental_usage_metadata_event(*, ev: AgentEvent, supplemental_pay
|
|
|
755
760
|
|
|
756
761
|
for supplemental_payload in supplemental_payloads:
|
|
757
762
|
supplemental_summary = extract_usage_metrics(supplemental_payload)
|
|
758
|
-
for field in ("model", "request_id"
|
|
763
|
+
for field in ("model", "request_id"):
|
|
759
764
|
current_value = extract_usage_metrics(merged_payload).get(field)
|
|
760
765
|
if isinstance(current_value, str) and current_value.strip():
|
|
761
766
|
continue
|
|
762
767
|
supplemental_value = supplemental_summary.get(field)
|
|
763
768
|
if isinstance(supplemental_value, str) and supplemental_value.strip():
|
|
764
769
|
merged_payload[field] = supplemental_value.strip()
|
|
770
|
+
current_provider = extract_usage_metrics(merged_payload).get("provider")
|
|
771
|
+
supplemental_provider = supplemental_summary.get("provider")
|
|
772
|
+
if not (isinstance(supplemental_provider, str) and supplemental_provider.strip()):
|
|
773
|
+
continue
|
|
774
|
+
supplemental_provider = supplemental_provider.strip()
|
|
775
|
+
supplemental_provider_is_placeholder = supplemental_provider in _USAGE_PROVIDER_PLACEHOLDERS
|
|
776
|
+
if not (isinstance(current_provider, str) and current_provider.strip()):
|
|
777
|
+
merged_payload["provider"] = supplemental_provider
|
|
778
|
+
continue
|
|
779
|
+
current_provider = current_provider.strip()
|
|
780
|
+
if (
|
|
781
|
+
current_provider in _USAGE_PROVIDER_PLACEHOLDERS
|
|
782
|
+
and not supplemental_provider_is_placeholder
|
|
783
|
+
and current_provider != supplemental_provider
|
|
784
|
+
):
|
|
785
|
+
merged_payload.setdefault("provider_upstream", current_provider)
|
|
786
|
+
merged_payload["provider"] = supplemental_provider
|
|
765
787
|
|
|
766
788
|
if merged_payload == upstream_payload:
|
|
767
789
|
return ev
|
|
@@ -118,6 +118,13 @@ def _backend_from_items(items):
|
|
|
118
118
|
async def _run_usage_backend(backend, tmp_path):
|
|
119
119
|
"""用 sdk_native Runtime 运行测试 backend,并返回 CapabilityResult。"""
|
|
120
120
|
|
|
121
|
+
rt = _make_usage_runtime(backend, tmp_path)
|
|
122
|
+
return await rt.run("agent.upstream_usage_with_sink", input={"prompt": "x"})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _make_usage_runtime(backend, tmp_path):
|
|
126
|
+
"""构造注册了 usage 测试 Agent 的 sdk_native Runtime。"""
|
|
127
|
+
|
|
121
128
|
rt = Runtime(
|
|
122
129
|
RuntimeConfig(
|
|
123
130
|
mode="sdk_native",
|
|
@@ -137,7 +144,7 @@ async def _run_usage_backend(backend, tmp_path):
|
|
|
137
144
|
),
|
|
138
145
|
)
|
|
139
146
|
)
|
|
140
|
-
return
|
|
147
|
+
return rt
|
|
141
148
|
|
|
142
149
|
|
|
143
150
|
@pytest.mark.asyncio
|
|
@@ -384,6 +391,150 @@ def test_supplemental_usage_metadata_helper_does_not_overwrite_existing_model()
|
|
|
384
391
|
}
|
|
385
392
|
|
|
386
393
|
|
|
394
|
+
@pytest.mark.parametrize("placeholder_provider", ["openai", "openai-compatible"])
|
|
395
|
+
def test_supplemental_usage_metadata_helper_overwrites_placeholder_provider(placeholder_provider: str) -> None:
|
|
396
|
+
ev = AgentEvent(
|
|
397
|
+
type="llm_usage",
|
|
398
|
+
timestamp="2026-05-05T00:00:00Z",
|
|
399
|
+
run_id="run_1",
|
|
400
|
+
payload={
|
|
401
|
+
"model": "upstream-model",
|
|
402
|
+
"input_tokens": 5,
|
|
403
|
+
"output_tokens": 6,
|
|
404
|
+
"total_tokens": 11,
|
|
405
|
+
"request_id": "req_upstream",
|
|
406
|
+
"provider": placeholder_provider,
|
|
407
|
+
},
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
merged = _merge_supplemental_usage_metadata_event(
|
|
411
|
+
ev=ev,
|
|
412
|
+
supplemental_payloads=[
|
|
413
|
+
{
|
|
414
|
+
"model": "sink-model",
|
|
415
|
+
"input_tokens": 99,
|
|
416
|
+
"output_tokens": 99,
|
|
417
|
+
"total_tokens": 198,
|
|
418
|
+
"request_id": "req_sink",
|
|
419
|
+
"provider": "gateway-provider",
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
assert merged.payload == {
|
|
425
|
+
"model": "upstream-model",
|
|
426
|
+
"input_tokens": 5,
|
|
427
|
+
"output_tokens": 6,
|
|
428
|
+
"total_tokens": 11,
|
|
429
|
+
"request_id": "req_upstream",
|
|
430
|
+
"provider": "gateway-provider",
|
|
431
|
+
"provider_upstream": placeholder_provider,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@pytest.mark.parametrize("sink_provider", ["", " ", "openai", "openai-compatible"])
|
|
436
|
+
def test_supplemental_usage_metadata_helper_keeps_placeholder_when_sink_is_not_effective(
|
|
437
|
+
sink_provider: str,
|
|
438
|
+
) -> None:
|
|
439
|
+
ev = AgentEvent(
|
|
440
|
+
type="llm_usage",
|
|
441
|
+
timestamp="2026-05-05T00:00:00Z",
|
|
442
|
+
run_id="run_1",
|
|
443
|
+
payload={
|
|
444
|
+
"input_tokens": 5,
|
|
445
|
+
"output_tokens": 6,
|
|
446
|
+
"total_tokens": 11,
|
|
447
|
+
"provider": "openai",
|
|
448
|
+
},
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
merged = _merge_supplemental_usage_metadata_event(
|
|
452
|
+
ev=ev,
|
|
453
|
+
supplemental_payloads=[{"provider": sink_provider}],
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
assert merged.payload == {
|
|
457
|
+
"input_tokens": 5,
|
|
458
|
+
"output_tokens": 6,
|
|
459
|
+
"total_tokens": 11,
|
|
460
|
+
"provider": "openai",
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@pytest.mark.parametrize("placeholder_provider", ["openai", "openai-compatible"])
|
|
465
|
+
@pytest.mark.asyncio
|
|
466
|
+
async def test_upstream_placeholder_provider_is_replaced_by_sink_effective_provider(
|
|
467
|
+
tmp_path,
|
|
468
|
+
placeholder_provider: str,
|
|
469
|
+
):
|
|
470
|
+
backend = _UpstreamUsageWithSinkBackend(
|
|
471
|
+
sink_payload={
|
|
472
|
+
"input_tokens": 99,
|
|
473
|
+
"output_tokens": 99,
|
|
474
|
+
"total_tokens": 198,
|
|
475
|
+
"request_id": "req_sink_gateway",
|
|
476
|
+
"provider": "gateway-provider",
|
|
477
|
+
},
|
|
478
|
+
completed_usage={"input_tokens": 5, "output_tokens": 6, "total_tokens": 11},
|
|
479
|
+
completed_request_id="req_gateway",
|
|
480
|
+
completed_provider=placeholder_provider,
|
|
481
|
+
)
|
|
482
|
+
rt = _make_usage_runtime(backend, tmp_path)
|
|
483
|
+
|
|
484
|
+
items = [
|
|
485
|
+
item
|
|
486
|
+
async for item in rt.run_stream("agent.upstream_usage_with_sink", input={"prompt": "x"})
|
|
487
|
+
]
|
|
488
|
+
usage_events = [item for item in items if isinstance(item, AgentEvent) and item.type == "llm_usage"]
|
|
489
|
+
result = items[-1]
|
|
490
|
+
|
|
491
|
+
assert usage_events
|
|
492
|
+
assert usage_events[-1].payload["provider"] == "gateway-provider"
|
|
493
|
+
assert usage_events[-1].payload["provider_upstream"] == placeholder_provider
|
|
494
|
+
assert result.node_report is not None
|
|
495
|
+
assert result.node_report.usage is not None
|
|
496
|
+
assert result.node_report.usage.input_tokens == 5
|
|
497
|
+
assert result.node_report.usage.output_tokens == 6
|
|
498
|
+
assert result.node_report.usage.total_tokens == 11
|
|
499
|
+
assert result.node_report.usage.request_id == "req_gateway"
|
|
500
|
+
assert result.node_report.usage.provider == "gateway-provider"
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
@pytest.mark.asyncio
|
|
504
|
+
async def test_upstream_placeholder_provider_keeps_placeholder_when_sink_is_placeholder(tmp_path):
|
|
505
|
+
backend = _UpstreamUsageWithSinkBackend(
|
|
506
|
+
sink_payload={
|
|
507
|
+
"input_tokens": 99,
|
|
508
|
+
"output_tokens": 99,
|
|
509
|
+
"total_tokens": 198,
|
|
510
|
+
"request_id": "req_sink_gateway",
|
|
511
|
+
"provider": "openai-compatible",
|
|
512
|
+
},
|
|
513
|
+
completed_usage={"input_tokens": 5, "output_tokens": 6, "total_tokens": 11},
|
|
514
|
+
completed_request_id="req_gateway",
|
|
515
|
+
completed_provider="openai",
|
|
516
|
+
)
|
|
517
|
+
rt = _make_usage_runtime(backend, tmp_path)
|
|
518
|
+
|
|
519
|
+
items = [
|
|
520
|
+
item
|
|
521
|
+
async for item in rt.run_stream("agent.upstream_usage_with_sink", input={"prompt": "x"})
|
|
522
|
+
]
|
|
523
|
+
usage_events = [item for item in items if isinstance(item, AgentEvent) and item.type == "llm_usage"]
|
|
524
|
+
result = items[-1]
|
|
525
|
+
|
|
526
|
+
assert usage_events
|
|
527
|
+
assert usage_events[-1].payload["provider"] == "openai"
|
|
528
|
+
assert "provider_upstream" not in usage_events[-1].payload
|
|
529
|
+
assert result.node_report is not None
|
|
530
|
+
assert result.node_report.usage is not None
|
|
531
|
+
assert result.node_report.usage.input_tokens == 5
|
|
532
|
+
assert result.node_report.usage.output_tokens == 6
|
|
533
|
+
assert result.node_report.usage.total_tokens == 11
|
|
534
|
+
assert result.node_report.usage.request_id == "req_gateway"
|
|
535
|
+
assert result.node_report.usage.provider == "openai"
|
|
536
|
+
|
|
537
|
+
|
|
387
538
|
@pytest.mark.asyncio
|
|
388
539
|
async def test_agently_backend_requests_include_usage_by_default_and_preserves_existing_stream_options():
|
|
389
540
|
requester = _FakeRequester(
|
|
@@ -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,
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_release_tag_version_guardrail.py
RENAMED
|
@@ -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
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/adapters/__init__.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 → capability_runtime-0.1.4}/src/capability_runtime/host_protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/history.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/host_toolkit/resume.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/logging_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/output_validator.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/__init__.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/agent.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/capability.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/context.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/protocol/workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/reporting/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/service_facade.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/structured_output.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/structured_stream.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/__init__.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/projector.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/session.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/store.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/transport.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/ui_events/v1.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/upstream_compat.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime/workflow_runtime.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/requires.txt
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/src/capability_runtime.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_bridge_artifacts_passthrough.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_bridge_register_tool_public_api.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_coding_agent_examples_atomic.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_coding_agent_examples_recipes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_docs_pinned_dependency_versions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_alignment_fixes_l1.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_examples_no_agent_sdk_imports.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → 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
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_approvals_profiles.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_history_assembler.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_invoke_capability.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_host_toolkit_resume_helper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_integration_approval_event_shape.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 → capability_runtime-0.1.4}/tests/test_project_identity_naming_matrix.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_replay_tool_calls_alignment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_bridge_fake_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_hitl_host_protocol.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_hooks_and_schema_gate.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_initial_history_and_meta.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_result_prompt_hardening.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_run_stream_semantics_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_service_session_bridge.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_structured_output_bridge.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_runtime_structured_stream.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_services_map_node_status.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_skills_conformance_smoke.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_chat_sse_usage_contract.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_compat_spaces_schema.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_prompt_profile_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_skills_bundles_contract.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_upstream_tool_descriptor_compat.py
RENAMED
|
File without changes
|
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_versioning_strategy_guard.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_wal_backend_injection_contract.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_wal_locator_resolution_contract.py
RENAMED
|
File without changes
|
{capability_runtime-0.1.3 → capability_runtime-0.1.4}/tests/test_workflow_host_runtime_surface.py
RENAMED
|
File without changes
|