capability-runtime 0.1.1__tar.gz → 0.1.3__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 (141) hide show
  1. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/PKG-INFO +1 -1
  2. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/pyproject.toml +1 -1
  3. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/__init__.py +1 -1
  4. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/adapters/agent_adapter.py +54 -10
  5. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/adapters/agently_backend.py +19 -3
  6. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/adapters/triggerflow_workflow_engine.py +2 -3
  7. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/host_protocol.py +9 -1
  8. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/reporting/node_report.py +20 -1
  9. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/runtime.py +46 -9
  10. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/sdk_lifecycle.py +66 -3
  11. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/structured_output.py +42 -16
  12. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/types.py +3 -0
  13. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/utils/usage.py +26 -6
  14. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime.egg-info/PKG-INFO +1 -1
  15. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime.egg-info/SOURCES.txt +1 -0
  16. capability_runtime-0.1.3/tests/test_agently_backend.py +479 -0
  17. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_node_report_builder.py +53 -0
  18. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_node_report_contract_v1.py +12 -0
  19. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_release_tag_version_guardrail.py +2 -2
  20. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_hitl_host_protocol.py +41 -0
  21. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_hooks_and_schema_gate.py +60 -0
  22. capability_runtime-0.1.3/tests/test_runtime_result_prompt_hardening.py +284 -0
  23. capability_runtime-0.1.1/tests/test_agently_backend.py +0 -201
  24. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/README.md +0 -0
  25. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/setup.cfg +0 -0
  26. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/adapters/__init__.py +0 -0
  27. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/adapters/workflow_engine.py +0 -0
  28. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/config.py +0 -0
  29. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/errors.py +0 -0
  30. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/guards.py +0 -0
  31. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/host_toolkit/__init__.py +0 -0
  32. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/host_toolkit/approvals_profiles.py +0 -0
  33. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/host_toolkit/evidence_hooks.py +0 -0
  34. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/host_toolkit/history.py +0 -0
  35. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/host_toolkit/invoke_capability.py +0 -0
  36. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/host_toolkit/resume.py +0 -0
  37. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/host_toolkit/system_prompt.py +0 -0
  38. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/host_toolkit/turn_delta.py +0 -0
  39. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/logging_utils.py +0 -0
  40. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/manifest.py +0 -0
  41. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/output_validator.py +0 -0
  42. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/protocol/__init__.py +0 -0
  43. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/protocol/agent.py +0 -0
  44. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/protocol/capability.py +0 -0
  45. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/protocol/chat_backend.py +0 -0
  46. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/protocol/context.py +0 -0
  47. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/protocol/workflow.py +0 -0
  48. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/registry.py +0 -0
  49. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/reporting/__init__.py +0 -0
  50. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/runtime_ui_events_mixin.py +0 -0
  51. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/service_facade.py +0 -0
  52. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/services.py +0 -0
  53. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/structured_stream.py +0 -0
  54. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/ui_events/__init__.py +0 -0
  55. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/ui_events/projector.py +0 -0
  56. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/ui_events/session.py +0 -0
  57. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/ui_events/store.py +0 -0
  58. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/ui_events/transport.py +0 -0
  59. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/ui_events/v1.py +0 -0
  60. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/upstream_compat.py +0 -0
  61. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/utils/__init__.py +0 -0
  62. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime/workflow_runtime.py +0 -0
  63. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime.egg-info/dependency_links.txt +0 -0
  64. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime.egg-info/requires.txt +0 -0
  65. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/src/capability_runtime.egg-info/top_level.txt +0 -0
  66. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_agently_backend_replay.py +0 -0
  67. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_backend_mode.py +0 -0
  68. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_bilingual_docs_surface.py +0 -0
  69. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_bridge_artifacts_passthrough.py +0 -0
  70. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_bridge_register_tool_public_api.py +0 -0
  71. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_coding_agent_examples_atomic.py +0 -0
  72. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_coding_agent_examples_recipes.py +0 -0
  73. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_config_glue.py +0 -0
  74. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_dependency_pins.py +0 -0
  75. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_docs_pinned_dependency_versions.py +0 -0
  76. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_docs_scheme2_no_skilladapter_residue.py +0 -0
  77. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_error_observability.py +0 -0
  78. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_alignment_fixes_l1.py +0 -0
  79. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_no_agent_sdk_imports.py +0 -0
  80. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_real_evidence_strict_integration.py +0 -0
  81. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_real_integration.py +0 -0
  82. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_smoke.py +0 -0
  83. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_ui_events_showcase_offline.py +0 -0
  84. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_ui_events_showcase_real_fallback.py +0 -0
  85. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_ui_events_showcase_real_integration.py +0 -0
  86. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_ui_events_showcase_ui_contract.py +0 -0
  87. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_examples_workflow_skills_first_smoke.py +0 -0
  88. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_guards.py +0 -0
  89. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_host_toolkit_approvals_profiles.py +0 -0
  90. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_host_toolkit_history_assembler.py +0 -0
  91. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_host_toolkit_invoke_capability.py +0 -0
  92. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_host_toolkit_invoke_capability_shared_runtime.py +0 -0
  93. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_host_toolkit_resume_helper.py +0 -0
  94. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_host_toolkit_system_prompt_evidence.py +0 -0
  95. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_host_toolkit_turn_delta.py +0 -0
  96. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_integration_agently_requester_smoke.py +0 -0
  97. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_integration_approval_event_shape.py +0 -0
  98. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_integration_sources_redis_pgsql_smoke.py +0 -0
  99. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_loop.py +0 -0
  100. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_node_report_engine_identity_contract.py +0 -0
  101. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_offline_backend_injection_evidence.py +0 -0
  102. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_per_capability_llm_config_model_routing.py +0 -0
  103. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_preflight_gate.py +0 -0
  104. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_project_identity_naming_matrix.py +0 -0
  105. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_public_api_exports.py +0 -0
  106. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_public_repo_hygiene.py +0 -0
  107. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_qa_agent.py +0 -0
  108. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_registry.py +0 -0
  109. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_replay_tool_calls_alignment.py +0 -0
  110. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_repo_hygiene_no_tracked_env_or_pyc.py +0 -0
  111. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_repo_no_deep_imports_in_user_facing_docs.py +0 -0
  112. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_review_followups_module_contracts.py +0 -0
  113. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_bridge_fake_backend.py +0 -0
  114. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_concurrency.py +0 -0
  115. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_engine.py +0 -0
  116. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_initial_history_and_meta.py +0 -0
  117. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_manifest.py +0 -0
  118. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_run_stream_semantics_v1.py +0 -0
  119. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_service_facade_rpc_mapping.py +0 -0
  120. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_service_session_bridge.py +0 -0
  121. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_status_mapping.py +0 -0
  122. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_structured_output_bridge.py +0 -0
  123. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_runtime_structured_stream.py +0 -0
  124. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_sandbox_permissions_passthrough_contract.py +0 -0
  125. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_services_call_callback.py +0 -0
  126. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_services_map_node_status.py +0 -0
  127. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_skills_conformance_smoke.py +0 -0
  128. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_adapter_tool_registration.py +0 -0
  129. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_chat_backend_protocol_contract.py +0 -0
  130. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_chat_sse_usage_contract.py +0 -0
  131. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_compat_spaces_schema.py +0 -0
  132. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_prompt_profile_contract.py +0 -0
  133. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_runtime_client_server_contract.py +0 -0
  134. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_sandbox_profile_precedence_contract.py +0 -0
  135. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_skills_bundles_contract.py +0 -0
  136. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_tool_descriptor_compat.py +0 -0
  137. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_upstream_verification.py +0 -0
  138. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_versioning_strategy_guard.py +0 -0
  139. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_wal_backend_injection_contract.py +0 -0
  140. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/tests/test_wal_locator_resolution_contract.py +0 -0
  141. {capability_runtime-0.1.1 → capability_runtime-0.1.3}/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.1
3
+ Version: 0.1.3
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.1"
3
+ version = "0.1.3"
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.1"
4
+ __version__ = "0.1.3"
5
5
 
6
6
  # === 统一入口 ===
7
7
  from .config import CustomTool, RuntimeConfig
@@ -80,6 +80,7 @@ class AgentAdapter:
80
80
  spec: AgentSpec,
81
81
  input: Dict[str, Any],
82
82
  context: ExecutionContext,
83
+ prompt_control: Optional[Dict[str, Any]] = None,
83
84
  ) -> AsyncIterator[Union[AgentEvent, CapabilityResult]]:
84
85
  """
85
86
  流式执行 AgentSpec:先 yield AgentEvent(若为真实执行),最后 yield CapabilityResult。
@@ -93,6 +94,7 @@ class AgentAdapter:
93
94
 
94
95
  备注:
95
96
  - 每条 `AgentEvent` 也会通过 Runtime 的内部 tap 旁路分发(用于 UI events 投影;不改变对外 `AgentEvent` 转发语义)。
97
+ - `prompt_control` 为显式 prompt 控制面,优先于兼容入口 `input["_runtime_prompt"]`。
96
98
  """
97
99
 
98
100
  mode = str(getattr(self._services.config, "mode", "mock"))
@@ -100,7 +102,12 @@ class AgentAdapter:
100
102
  yield await self._mock_execute(spec=spec, input=input, context=context)
101
103
  return
102
104
 
103
- async for item in self._bridge_execute_stream(spec=spec, input=input, context=context):
105
+ async for item in self._bridge_execute_stream(
106
+ spec=spec,
107
+ input=input,
108
+ context=context,
109
+ prompt_control=prompt_control,
110
+ ):
104
111
  yield item
105
112
 
106
113
  async def _mock_execute(self, *, spec: AgentSpec, input: Dict[str, Any], context: ExecutionContext) -> CapabilityResult:
@@ -193,14 +200,26 @@ class AgentAdapter:
193
200
  )
194
201
 
195
202
  async def _bridge_execute_stream(
196
- self, *, spec: AgentSpec, input: Dict[str, Any], context: ExecutionContext
203
+ self,
204
+ *,
205
+ spec: AgentSpec,
206
+ input: Dict[str, Any],
207
+ context: ExecutionContext,
208
+ prompt_control: Optional[Dict[str, Any]] = None,
197
209
  ) -> AsyncIterator[Union[AgentEvent, CapabilityResult]]:
198
210
  """
199
211
  真实执行(bridge/sdk_native):驱动 SDK Agent.run_stream_async 并聚合 NodeReport。
212
+
213
+ 参数:
214
+ - prompt_control:显式 prompt 控制面;为空时回退读取 `input["_runtime_prompt"]`。
200
215
  """
201
216
 
202
217
  try:
203
- prompt_plan = self._resolve_prompt_render_plan(spec=spec, input=input)
218
+ prompt_plan = self._resolve_prompt_render_plan(
219
+ spec=spec,
220
+ input=input,
221
+ prompt_control=prompt_control,
222
+ )
204
223
  except _InvalidPromptMessages as exc:
205
224
  report = self._services.build_fail_closed_report(
206
225
  run_id=context.run_id,
@@ -283,6 +302,19 @@ class AgentAdapter:
283
302
  events: List[AgentEvent] = []
284
303
  host_meta = self._services.get_host_meta(context=context)
285
304
  initial_history = host_meta.get("initial_history") if isinstance(host_meta.get("initial_history"), list) else None
305
+ on_event_error_count = 0
306
+ on_event_last_error_type: Optional[str] = None
307
+
308
+ def _apply_on_event_error_evidence(report: Any) -> None:
309
+ """把 on_event fail-open 错误摘要写入 NodeReport.meta。"""
310
+
311
+ if on_event_error_count <= 0:
312
+ return
313
+ meta = getattr(report, "meta", None)
314
+ if isinstance(meta, dict):
315
+ meta["on_event_error_count"] = on_event_error_count
316
+ if on_event_last_error_type:
317
+ meta["on_event_last_error_type"] = on_event_last_error_type
286
318
 
287
319
  try:
288
320
  async for ev in agent.run_stream_async(task, run_id=context.run_id, initial_history=initial_history):
@@ -306,6 +338,8 @@ class AgentAdapter:
306
338
  {"run_id": context.run_id, "capability_id": spec.base.id},
307
339
  )
308
340
  except Exception as cb_exc:
341
+ on_event_error_count += 1
342
+ on_event_last_error_type = type(cb_exc).__name__
309
343
  log_suppressed_exception(
310
344
  context="on_event_callback",
311
345
  exc=cb_exc,
@@ -323,6 +357,7 @@ class AgentAdapter:
323
357
  meta={"capability_id": spec.base.id, "source": "sdk_agent"},
324
358
  )
325
359
  self._apply_prompt_evidence(report=report, evidence=prompt_plan.evidence)
360
+ _apply_on_event_error_evidence(report)
326
361
  yield CapabilityResult(
327
362
  status=CapabilityStatus.CANCELLED,
328
363
  error="execution cancelled",
@@ -339,6 +374,7 @@ class AgentAdapter:
339
374
  completion_reason="engine_exception",
340
375
  meta={"engine_exception": type(exc).__name__, **prompt_plan.evidence},
341
376
  )
377
+ _apply_on_event_error_evidence(report)
342
378
  yield CapabilityResult(
343
379
  status=CapabilityStatus.FAILED,
344
380
  error=str(exc),
@@ -350,6 +386,7 @@ class AgentAdapter:
350
386
 
351
387
  report = NodeReportBuilder().build(events=events)
352
388
  self._apply_prompt_evidence(report=report, evidence=prompt_plan.evidence)
389
+ _apply_on_event_error_evidence(report)
353
390
  if issues and getattr(self._services.config, "preflight_mode", "error") == "warn":
354
391
  report.meta["preflight_mode"] = "warn"
355
392
  report.meta["preflight_issues"] = [self._services.redact_issue(i) for i in issues]
@@ -389,13 +426,20 @@ class AgentAdapter:
389
426
  artifacts=list(report.artifacts),
390
427
  )
391
428
 
392
- def _resolve_prompt_render_plan(self, *, spec: AgentSpec, input: Dict[str, Any]) -> _PromptRenderPlan:
429
+ def _resolve_prompt_render_plan(
430
+ self,
431
+ *,
432
+ spec: AgentSpec,
433
+ input: Dict[str, Any],
434
+ prompt_control: Optional[Dict[str, Any]] = None,
435
+ ) -> _PromptRenderPlan:
393
436
  """
394
437
  解析 AgentSpec + run-level `_runtime_prompt`,生成 prompt 渲染计划。
395
438
 
396
439
  参数:
397
440
  - spec:Agent 声明
398
441
  - input:run 输入,其中 `_runtime_prompt` 为保留控制面
442
+ - prompt_control:显式控制面;若提供则优先于 input 内兼容保留键
399
443
 
400
444
  返回:
401
445
  - `_PromptRenderPlan`
@@ -404,13 +448,14 @@ class AgentAdapter:
404
448
  - `_InvalidPromptMessages`:控制面非法,调用方应 fail-fast。
405
449
  """
406
450
 
407
- envelope_raw = input.get(_RUNTIME_PROMPT_KEY)
451
+ envelope_raw = prompt_control if prompt_control is not None else input.get(_RUNTIME_PROMPT_KEY)
452
+ envelope_name = "prompt_control" if prompt_control is not None else "_runtime_prompt"
408
453
  if envelope_raw is None:
409
454
  envelope: Dict[str, Any] = {}
410
455
  elif isinstance(envelope_raw, dict):
411
456
  envelope = dict(envelope_raw)
412
457
  else:
413
- raise _InvalidPromptMessages("_runtime_prompt must be a dict")
458
+ raise _InvalidPromptMessages(f"{envelope_name} must be a dict")
414
459
 
415
460
  raw_mode = envelope.get("mode") if "mode" in envelope else getattr(spec, "prompt_render_mode", "structured_task")
416
461
  if raw_mode is None:
@@ -570,10 +615,9 @@ class AgentAdapter:
570
615
  if business_input:
571
616
  lines: List[str] = []
572
617
  for k, v in business_input.items():
573
- if isinstance(v, str):
574
- lines.append(f"- {k}: {v}")
575
- else:
576
- lines.append(f"- {k}: {json.dumps(v, ensure_ascii=False)}")
618
+ key_text = json.dumps(str(k), ensure_ascii=False)
619
+ value_text = json.dumps(v, ensure_ascii=False)
620
+ lines.append(f"- {key_text}: {value_text}")
577
621
  parts.append(f"{_SECTION_INPUT}\n" + "\n".join(lines))
578
622
 
579
623
  if spec.output_schema and spec.output_schema.fields:
@@ -66,13 +66,19 @@ class AgentlyBackendConfig:
66
66
 
67
67
  requester_factory: AgentlyRequesterFactory
68
68
 
69
- def _normalize_usage_payload(*, usage: Any, model: Any = None) -> Optional[Dict[str, Any]]:
69
+ def _normalize_usage_payload(
70
+ *,
71
+ usage: Any,
72
+ model: Any = None,
73
+ request_id: Any = None,
74
+ provider: Any = None,
75
+ ) -> Optional[Dict[str, Any]]:
70
76
  """
71
77
  把 provider usage 归一为 capability-runtime 的 `llm_usage` payload 形状。
72
78
 
73
79
  返回:
74
80
  - `None`:无法提取任何有效 usage 字段
75
- - `dict`:`model/input_tokens/output_tokens/total_tokens`
81
+ - `dict`:`model/input_tokens/output_tokens/total_tokens/request_id/provider`
76
82
  """
77
83
 
78
84
  if not isinstance(usage, dict):
@@ -93,6 +99,8 @@ def _normalize_usage_payload(*, usage: Any, model: Any = None) -> Optional[Dict[
93
99
  "input_tokens": input_tokens,
94
100
  "output_tokens": output_tokens,
95
101
  "total_tokens": total_tokens,
102
+ "request_id": request_id.strip() if isinstance(request_id, str) and request_id.strip() else None,
103
+ "provider": provider.strip() if isinstance(provider, str) and provider.strip() else None,
96
104
  }
97
105
  return payload if any(value is not None for value in payload.values()) else None
98
106
 
@@ -120,7 +128,15 @@ def _extract_usage_payload_from_sse_data(data: str) -> Optional[Dict[str, Any]]:
120
128
  return None
121
129
  if not isinstance(obj, dict):
122
130
  return None
123
- return _normalize_usage_payload(usage=obj.get("usage"), model=obj.get("model"))
131
+ request_id = obj.get("request_id")
132
+ if not (isinstance(request_id, str) and request_id.strip()):
133
+ request_id = obj.get("id")
134
+ return _normalize_usage_payload(
135
+ usage=obj.get("usage"),
136
+ model=obj.get("model"),
137
+ request_id=request_id,
138
+ provider=obj.get("provider"),
139
+ )
124
140
 
125
141
 
126
142
  def _merge_stream_options_for_usage(value: Any) -> Dict[str, Any]:
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  import asyncio
5
5
  import uuid
6
6
  from contextlib import suppress
7
- from dataclasses import dataclass
7
+ from dataclasses import dataclass, replace
8
8
  from types import MappingProxyType
9
9
  from typing import Any, AsyncIterator, Dict, List, Optional, cast
10
10
 
@@ -598,8 +598,7 @@ class TriggerFlowWorkflowEngine:
598
598
  completion_reason="loop_iteration_failed",
599
599
  meta={"step_id": step.id, "capability_id": step.capability.id},
600
600
  )
601
- result.report = report
602
- result.node_report = report
601
+ result = replace(result, report=report, node_report=report)
603
602
 
604
603
  context.step_outputs[step.id] = result.output
605
604
  context.step_results[step.id] = _to_step_result_dict(result)
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  """HITL / wait-resume / approval 的宿主协议收敛层。"""
4
4
 
5
+ import html
6
+ import re
5
7
  from dataclasses import dataclass, field
6
8
  from enum import Enum
7
9
  from typing import Any, Optional
@@ -397,4 +399,10 @@ def _build_message_preview(value: Any) -> str | None:
397
399
  normalized = " ".join(value.split())
398
400
  if not normalized:
399
401
  return None
400
- return normalized[:120]
402
+ secret_pattern = re.compile(
403
+ r"(?i)\b(password|passwd|api_key|apikey|token|access_token|secret)\s*=\s*"
404
+ r"(\"[^\"]*\"|'[^']*'|[^\s&]+)"
405
+ )
406
+ redacted = secret_pattern.sub(lambda m: f"{m.group(1)}=[REDACTED]", normalized)
407
+ redacted = re.sub(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+", "Bearer [REDACTED]", redacted)
408
+ return html.escape(redacted, quote=True)[:120]
@@ -161,6 +161,8 @@ class NodeReportBuilder:
161
161
  usage_input_seen = False
162
162
  usage_output_seen = False
163
163
  usage_total_seen = False
164
+ usage_request_id: Optional[str] = None
165
+ usage_provider: Optional[str] = None
164
166
 
165
167
  def _ensure_tool(call_id: str, *, name: str) -> Dict[str, Any]:
166
168
  """获取/初始化工具调用聚合槽位(以 call_id 为主键)。"""
@@ -217,6 +219,14 @@ class NodeReportBuilder:
217
219
  if isinstance(model_name, str) and model_name.strip():
218
220
  usage_model = model_name.strip()
219
221
 
222
+ request_id = usage_summary.get("request_id")
223
+ if isinstance(request_id, str) and request_id.strip():
224
+ usage_request_id = request_id.strip()
225
+
226
+ provider = usage_summary.get("provider")
227
+ if isinstance(provider, str) and provider.strip():
228
+ usage_provider = provider.strip()
229
+
220
230
  input_tokens = usage_summary.get("input_tokens")
221
231
  if isinstance(input_tokens, int):
222
232
  usage_input_total += input_tokens
@@ -378,12 +388,21 @@ class NodeReportBuilder:
378
388
  NodeToolCallReport.model_validate(item) for item in tool_calls.values() if isinstance(item, dict)
379
389
  ]
380
390
  usage_report: Optional[NodeUsageReport] = None
381
- if usage_model is not None or usage_input_seen or usage_output_seen or usage_total_seen:
391
+ if (
392
+ usage_model is not None
393
+ or usage_input_seen
394
+ or usage_output_seen
395
+ or usage_total_seen
396
+ or usage_request_id is not None
397
+ or usage_provider is not None
398
+ ):
382
399
  usage_report = NodeUsageReport(
383
400
  model=usage_model,
384
401
  input_tokens=usage_input_total if usage_input_seen else None,
385
402
  output_tokens=usage_output_total if usage_output_seen else None,
386
403
  total_tokens=usage_total_total if usage_total_seen else None,
404
+ request_id=usage_request_id,
405
+ provider=usage_provider,
387
406
  )
388
407
 
389
408
  report = NodeReport(
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  """
11
11
 
12
12
  import asyncio
13
+ import copy
13
14
  import time
14
15
  import uuid
15
16
  from dataclasses import replace
@@ -344,6 +345,7 @@ class Runtime(RuntimeUIEventsMixin):
344
345
  *,
345
346
  input: Optional[Dict[str, Any]] = None,
346
347
  context: Optional[ExecutionContext] = None,
348
+ prompt_control: Optional[Dict[str, Any]] = None,
347
349
  ) -> CapabilityResult:
348
350
  """
349
351
  非流式执行(等待完成后返回)。
@@ -352,10 +354,14 @@ class Runtime(RuntimeUIEventsMixin):
352
354
  - capability_id:能力 ID
353
355
  - input:输入参数 dict
354
356
  - context:可选执行上下文(宿主控制;若不传则由 Runtime 创建)
357
+ - prompt_control:可选 prompt 控制面;优先于兼容入口 `input["_runtime_prompt"]`
355
358
  """
356
359
 
357
360
  result: Optional[CapabilityResult] = None
358
- async for item in self.run_stream(capability_id, input=input, context=context):
361
+ stream_kwargs: Dict[str, Any] = {"input": input, "context": context}
362
+ if prompt_control is not None:
363
+ stream_kwargs["prompt_control"] = prompt_control
364
+ async for item in self.run_stream(capability_id, **stream_kwargs):
359
365
  if isinstance(item, CapabilityResult):
360
366
  result = item
361
367
  if result is None:
@@ -373,6 +379,7 @@ class Runtime(RuntimeUIEventsMixin):
373
379
  *,
374
380
  input: Optional[Dict[str, Any]] = None,
375
381
  context: Optional[ExecutionContext] = None,
382
+ prompt_control: Optional[Dict[str, Any]] = None,
376
383
  ) -> CapabilityResult:
377
384
  """
378
385
  强结构输出入口:成功时返回 `dict` 输出。
@@ -380,11 +387,12 @@ class Runtime(RuntimeUIEventsMixin):
380
387
  约束:
381
388
  - 仅支持带 `output_schema` 的 Agent capability;
382
389
  - 不改变 `run()` 的既有语义,而是在其之上做强结构收口。
390
+ - `prompt_control` 透传给底层 `run()`,用于显式控制 prompt rendering。
383
391
  """
384
392
 
385
393
  spec = self._registry.get(capability_id)
386
394
  if spec is None:
387
- return await self.run(capability_id, input=input, context=context)
395
+ return await self.run(capability_id, input=input, context=context, prompt_control=prompt_control)
388
396
  if not isinstance(spec, AgentSpec):
389
397
  run_id = context.run_id if context is not None else uuid.uuid4().hex
390
398
  return self._build_fail_closed_result(
@@ -408,7 +416,7 @@ class Runtime(RuntimeUIEventsMixin):
408
416
  completion_reason="structured_output_schema_missing",
409
417
  )
410
418
 
411
- result = await self.run(capability_id, input=input, context=context)
419
+ result = await self.run(capability_id, input=input, context=context, prompt_control=prompt_control)
412
420
  if result.status != CapabilityStatus.SUCCESS:
413
421
  return result
414
422
 
@@ -426,6 +434,7 @@ class Runtime(RuntimeUIEventsMixin):
426
434
  *,
427
435
  input: Optional[Dict[str, Any]] = None,
428
436
  context: Optional[ExecutionContext] = None,
437
+ prompt_control: Optional[Dict[str, Any]] = None,
429
438
  ) -> AsyncIterator[StructuredStreamEvent]:
430
439
  """
431
440
  结构化输出流式消费入口。
@@ -433,6 +442,7 @@ class Runtime(RuntimeUIEventsMixin):
433
442
  说明:
434
443
  - 面向业务代码,不透传 mixed stream / UI events 的全部细节;
435
444
  - 仅在观察到 `llm_response_delta(text)` 时产出中途快照与字段更新。
445
+ - `prompt_control` 透传给底层 `run_stream()`,优先于 `input["_runtime_prompt"]`。
436
446
  """
437
447
 
438
448
  spec = self._registry.get(capability_id)
@@ -474,7 +484,10 @@ class Runtime(RuntimeUIEventsMixin):
474
484
  previous_snapshot: Optional[Dict[str, Any]] = None
475
485
  terminal: Optional[CapabilityResult] = None
476
486
 
477
- async for item in self.run_stream(capability_id, input=input, context=ctx):
487
+ stream_kwargs: Dict[str, Any] = {"input": input, "context": ctx}
488
+ if prompt_control is not None:
489
+ stream_kwargs["prompt_control"] = prompt_control
490
+ async for item in self.run_stream(capability_id, **stream_kwargs):
478
491
  if isinstance(item, CapabilityResult):
479
492
  terminal = item
480
493
  continue
@@ -564,6 +577,7 @@ class Runtime(RuntimeUIEventsMixin):
564
577
  *,
565
578
  input: Optional[Dict[str, Any]] = None,
566
579
  context: Optional[ExecutionContext] = None,
580
+ prompt_control: Optional[Dict[str, Any]] = None,
567
581
  ) -> AsyncIterator[Union[AgentEvent, WorkflowStreamEvent, CapabilityResult]]:
568
582
  """
569
583
  流式执行(执行层混合流):过程中可能产出事件,最后产出终态 CapabilityResult。
@@ -610,15 +624,30 @@ class Runtime(RuntimeUIEventsMixin):
610
624
 
611
625
  started = time.monotonic()
612
626
  if _get_base(spec).kind == CapabilityKind.AGENT:
613
- async for x in self._execute_agent_stream(spec=spec, input=input or {}, context=ctx):
627
+ async for x in self._execute_agent_stream(
628
+ spec=spec,
629
+ input=input or {},
630
+ context=ctx,
631
+ prompt_control=prompt_control,
632
+ ):
614
633
  if isinstance(x, CapabilityResult):
615
- x.duration_ms = (time.monotonic() - started) * 1000
634
+ x = replace(
635
+ x,
636
+ duration_ms=(time.monotonic() - started) * 1000,
637
+ artifacts=list(x.artifacts or []),
638
+ metadata=copy.deepcopy(x.metadata or {}),
639
+ )
616
640
  yield x
617
641
  return
618
642
 
619
643
  async for x in self._execute_workflow_stream(spec=spec, input=input or {}, context=ctx):
620
644
  if isinstance(x, CapabilityResult):
621
- x.duration_ms = (time.monotonic() - started) * 1000
645
+ x = replace(
646
+ x,
647
+ duration_ms=(time.monotonic() - started) * 1000,
648
+ artifacts=list(x.artifacts or []),
649
+ metadata=copy.deepcopy(x.metadata or {}),
650
+ )
622
651
  yield x
623
652
  return
624
653
 
@@ -826,7 +855,12 @@ class Runtime(RuntimeUIEventsMixin):
826
855
  yield item
827
856
 
828
857
  async def _execute_agent_stream(
829
- self, *, spec: AnySpec, input: Dict[str, Any], context: ExecutionContext
858
+ self,
859
+ *,
860
+ spec: AnySpec,
861
+ input: Dict[str, Any],
862
+ context: ExecutionContext,
863
+ prompt_control: Optional[Dict[str, Any]] = None,
830
864
  ) -> AsyncIterator[Union[AgentEvent, CapabilityResult]]:
831
865
  """
832
866
  执行 AgentSpec(流式)。
@@ -849,7 +883,10 @@ class Runtime(RuntimeUIEventsMixin):
849
883
  )
850
884
  return
851
885
 
852
- async for item in self._agent_adapter.execute_stream(spec=spec, input=input, context=context):
886
+ execute_kwargs: Dict[str, Any] = {"spec": spec, "input": input, "context": context}
887
+ if prompt_control is not None:
888
+ execute_kwargs["prompt_control"] = prompt_control
889
+ async for item in self._agent_adapter.execute_stream(**execute_kwargs):
853
890
  yield item
854
891
 
855
892
  def build_fail_closed_report(
@@ -19,6 +19,7 @@ from skills_runtime.skills.manager import SkillsManager
19
19
  from .config import CustomTool, RuntimeConfig, RuntimeMode, normalize_workspace_root
20
20
  from .logging_utils import log_suppressed_exception
21
21
  from .protocol.chat_backend import ChatBackendProtocol
22
+ from .utils.usage import extract_usage_metrics
22
23
 
23
24
 
24
25
  @dataclass(frozen=True)
@@ -668,7 +669,7 @@ class _UsageTapBackend:
668
669
 
669
670
  class _AgentUsageEventBridge:
670
671
  """
671
- Agent 薄代理:在底层未主动发出 `llm_usage` 时,补发 bridge 收集到的 usage AgentEvent。
672
+ Agent 薄代理:补齐 bridge usage metadata,或在底层未主动发出 `llm_usage` 时补发 usage 事件。
672
673
  """
673
674
 
674
675
  def __init__(self, *, agent: Any, usage_bridge_backend: _UsageTapBackend) -> None:
@@ -683,16 +684,32 @@ class _AgentUsageEventBridge:
683
684
  initial_history: Optional[List[Dict[str, Any]]] = None,
684
685
  ) -> AsyncIterator[AgentEvent]:
685
686
  """
686
- 转发 SDK Agent 事件流;若未见上游 `llm_usage`,在流结束后补发 bridge usage 事件。
687
+ 转发 SDK Agent 事件流,并按 token usage bridge 规格处理 supplemental usage
688
+
689
+ 参数:
690
+ - task:传给上游 Agent 的任务文本
691
+ - run_id:可选 run 标识
692
+ - initial_history:可选初始历史
693
+
694
+ 返回:
695
+ - AgentEvent 异步流;上游 `llm_usage` 存在时只补齐缺失 metadata,不重复补 token
687
696
  """
688
697
 
689
698
  saw_llm_usage = False
699
+ pending_supplemental_usage: List[Dict[str, Any]] = []
690
700
  last_run_id = run_id or ""
691
701
  last_turn_id: Optional[str] = None
692
702
 
693
703
  async for ev in self._agent.run_stream_async(task, run_id=run_id, initial_history=initial_history):
694
704
  if ev.type == "llm_usage":
695
705
  saw_llm_usage = True
706
+ pending_supplemental_usage.extend(self._usage_bridge_backend.drain_usage_payloads())
707
+ if pending_supplemental_usage:
708
+ ev = _merge_supplemental_usage_metadata_event(
709
+ ev=ev,
710
+ supplemental_payloads=pending_supplemental_usage,
711
+ )
712
+ pending_supplemental_usage.clear()
696
713
  if isinstance(ev.run_id, str) and ev.run_id:
697
714
  last_run_id = ev.run_id
698
715
  if isinstance(ev.turn_id, str) and ev.turn_id:
@@ -700,10 +717,12 @@ class _AgentUsageEventBridge:
700
717
  yield ev
701
718
 
702
719
  if saw_llm_usage:
720
+ # 上游 usage 已是 token 权威来源;清空未配对 payload,避免作为新 usage 事件造成双计。
703
721
  self._usage_bridge_backend.drain_usage_payloads()
704
722
  return
705
723
 
706
- for payload in self._usage_bridge_backend.drain_usage_payloads():
724
+ pending_supplemental_usage.extend(self._usage_bridge_backend.drain_usage_payloads())
725
+ for payload in pending_supplemental_usage:
707
726
  yield AgentEvent(
708
727
  type="llm_usage",
709
728
  timestamp=_now_rfc3339(),
@@ -719,6 +738,50 @@ def _now_rfc3339() -> str:
719
738
  return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
720
739
 
721
740
 
741
+ def _merge_supplemental_usage_metadata_event(*, ev: AgentEvent, supplemental_payloads: List[Dict[str, Any]]) -> AgentEvent:
742
+ """
743
+ 把 sink payload 作为 supplemental metadata patch 合并到上游 `llm_usage` 事件。
744
+
745
+ 参数:
746
+ - ev:上游 SDK / agent loop 已发出的 `llm_usage` AgentEvent
747
+ - supplemental_payloads:`_caprt_usage_sink` 收集到的 provider/gateway usage payload 列表
748
+
749
+ 返回:
750
+ - 新 AgentEvent;仅在上游 metadata 缺失时补 `model/request_id/provider`,不写入 token 字段
751
+ """
752
+
753
+ upstream_payload = dict(ev.payload) if isinstance(ev.payload, dict) else {}
754
+ merged_payload = dict(upstream_payload)
755
+
756
+ for supplemental_payload in supplemental_payloads:
757
+ supplemental_summary = extract_usage_metrics(supplemental_payload)
758
+ for field in ("model", "request_id", "provider"):
759
+ current_value = extract_usage_metrics(merged_payload).get(field)
760
+ if isinstance(current_value, str) and current_value.strip():
761
+ continue
762
+ supplemental_value = supplemental_summary.get(field)
763
+ if isinstance(supplemental_value, str) and supplemental_value.strip():
764
+ merged_payload[field] = supplemental_value.strip()
765
+
766
+ if merged_payload == upstream_payload:
767
+ return ev
768
+ try:
769
+ return ev.model_copy(update={"payload": merged_payload})
770
+ except Exception as exc: # pragma: no cover - 当前 AgentEvent 为 Pydantic v2,保留兼容兜底。
771
+ log_suppressed_exception(
772
+ context="supplemental_usage_event_model_copy",
773
+ exc=exc,
774
+ )
775
+ return AgentEvent(
776
+ type=ev.type,
777
+ timestamp=ev.timestamp,
778
+ run_id=ev.run_id,
779
+ turn_id=ev.turn_id,
780
+ step_id=ev.step_id,
781
+ payload=merged_payload,
782
+ )
783
+
784
+
722
785
  def _merge_usage_sink(*, extra: Dict[str, Any], sink: Any) -> Dict[str, Any]:
723
786
  """
724
787
  合并 `_caprt_usage_sink` 回调,保留已有 sink 并保证两者都被调用。