power-loop 7.1.0__tar.gz → 7.3.0__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 (125) hide show
  1. {power_loop-7.1.0 → power_loop-7.3.0}/PKG-INFO +1 -1
  2. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/__init__.py +4 -1
  3. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/interface.py +4 -0
  4. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/llm_factory.py +9 -0
  5. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/agent/stateful_loop.py +13 -1
  6. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/agent/types.py +6 -0
  7. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/event_payloads.py +7 -0
  8. power_loop-7.3.0/power_loop/core/pass_turn.py +82 -0
  9. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/core/pipeline.py +54 -1
  10. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop.egg-info/PKG-INFO +1 -1
  11. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop.egg-info/SOURCES.txt +1 -0
  12. {power_loop-7.1.0 → power_loop-7.3.0}/LICENSE +0 -0
  13. {power_loop-7.1.0 → power_loop-7.3.0}/README.md +0 -0
  14. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/__init__.py +0 -0
  15. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/__init__.py +0 -0
  16. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
  17. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/attempt_scope.py +0 -0
  18. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/capabilities.py +0 -0
  19. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
  20. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
  21. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/multimodal.py +0 -0
  22. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/_vendor/llm_client/proxy_fallback.py +0 -0
  23. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/agent/__init__.py +0 -0
  24. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/agent/follow_up.py +0 -0
  25. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/agent/sink.py +0 -0
  26. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/agent/system_prompt.py +0 -0
  27. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/__init__.py +0 -0
  28. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/context_policy.py +0 -0
  29. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/errors.py +0 -0
  30. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/events.py +0 -0
  31. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/handlers.py +0 -0
  32. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/hook_contexts.py +0 -0
  33. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/hooks.py +0 -0
  34. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/messages.py +0 -0
  35. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/protocols.py +0 -0
  36. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contracts/tools.py +0 -0
  37. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contrib/__init__.py +0 -0
  38. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contrib/_redact.py +0 -0
  39. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contrib/jsonl_sink.py +0 -0
  40. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contrib/logging_sink.py +0 -0
  41. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contrib/mcp.py +0 -0
  42. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contrib/metrics_sink.py +0 -0
  43. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/contrib/otel_sink.py +0 -0
  44. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/core/agent_context.py +0 -0
  45. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/core/events.py +0 -0
  46. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/core/hooks.py +0 -0
  47. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/core/phase.py +0 -0
  48. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/core/runner.py +0 -0
  49. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/core/state.py +0 -0
  50. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/py.typed +0 -0
  51. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/background.py +0 -0
  52. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/bash_capture.py +0 -0
  53. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/blackboard.py +0 -0
  54. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/budget.py +0 -0
  55. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/cancellation.py +0 -0
  56. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/capability_probe.py +0 -0
  57. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/compact.py +0 -0
  58. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/context_worker.py +0 -0
  59. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/env.py +0 -0
  60. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/exec_backend.py +0 -0
  61. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/fold.py +0 -0
  62. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/fold_adapter.py +0 -0
  63. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/history_projector.py +0 -0
  64. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/history_sanitize.py +0 -0
  65. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/human_input.py +0 -0
  66. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/image_recall.py +0 -0
  67. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/lineage.py +0 -0
  68. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/memory.py +0 -0
  69. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/notes.py +0 -0
  70. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/provider.py +0 -0
  71. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/representation.py +0 -0
  72. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/retry.py +0 -0
  73. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/runtime_state.py +0 -0
  74. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/session_store.py +0 -0
  75. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/skills.py +0 -0
  76. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/spec.py +0 -0
  77. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/steer.py +0 -0
  78. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/stop_policy.py +0 -0
  79. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/__init__.py +0 -0
  80. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/backends/__init__.py +0 -0
  81. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/backends/mysql.py +0 -0
  82. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/backends/postgres.py +0 -0
  83. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/backends/sqlite.py +0 -0
  84. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/capabilities.py +0 -0
  85. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/context_cache.py +0 -0
  86. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/db.py +0 -0
  87. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/dialect.py +0 -0
  88. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/factory.py +0 -0
  89. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/schema.py +0 -0
  90. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/store.py +0 -0
  91. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/store/types.py +0 -0
  92. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/structured.py +0 -0
  93. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/stub_provider.py +0 -0
  94. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/timers.py +0 -0
  95. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/todos.py +0 -0
  96. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/tool_archive.py +0 -0
  97. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/tool_context.py +0 -0
  98. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/tool_truncation.py +0 -0
  99. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/runtime/usage_estimate.py +0 -0
  100. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/tools/__init__.py +0 -0
  101. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/tools/blackboard.py +0 -0
  102. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/tools/command_policy.py +0 -0
  103. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/tools/command_rules.py +0 -0
  104. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/tools/default_manifest.py +0 -0
  105. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/tools/default_tools.py +0 -0
  106. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/tools/registry.py +0 -0
  107. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/tools/spawn_agent.py +0 -0
  108. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/__init__.py +0 -0
  109. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/admission.py +0 -0
  110. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/api.py +0 -0
  111. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/engine.py +0 -0
  112. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/introspect.py +0 -0
  113. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/journal.py +0 -0
  114. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/result.py +0 -0
  115. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/resume.py +0 -0
  116. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/runner.py +0 -0
  117. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/spec.py +0 -0
  118. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/subprocess_executor.py +0 -0
  119. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/tool.py +0 -0
  120. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop/workflow/worker.py +0 -0
  121. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop.egg-info/dependency_links.txt +0 -0
  122. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop.egg-info/requires.txt +0 -0
  123. {power_loop-7.1.0 → power_loop-7.3.0}/power_loop.egg-info/top_level.txt +0 -0
  124. {power_loop-7.1.0 → power_loop-7.3.0}/pyproject.toml +0 -0
  125. {power_loop-7.1.0 → power_loop-7.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 7.1.0
3
+ Version: 7.3.0
4
4
  Summary: Embeddable agent execution kernel — LLM loop, hooks, events, tools, dynamic sub-agents.
5
5
  Author-email: zhangran <zhangran24@126.com>
6
6
  License: MIT
@@ -15,7 +15,7 @@ Stability tiers
15
15
  无版本承诺,可随时变更或删除。
16
16
  """
17
17
 
18
- __version__ = "7.1.0"
18
+ __version__ = "7.3.0"
19
19
 
20
20
  # Public LLM contract (SDK-free) re-exported so callers (e.g. writing llm.* hooks or
21
21
  # a custom LLMService) don't reach into the internal vendored transport package (H3.4).
@@ -140,6 +140,7 @@ from power_loop.contracts.tools import ToolDefinition, validate_tool_args
140
140
  from power_loop.core.agent_context import get_effective_tools
141
141
  from power_loop.core.events import AgentEventBus
142
142
  from power_loop.core.hooks import AgentHooks
143
+ from power_loop.core.pass_turn import PASS_TURN_TOOL, request_pass
143
144
  from power_loop.core.phase import PhaseContext, PhaseResult, phase
144
145
  from power_loop.core.pipeline import AgentPipeline
145
146
  from power_loop.core.runner import AgentRunner
@@ -303,6 +304,8 @@ STABLE_API = (
303
304
  )
304
305
 
305
306
  __all__ = [
307
+ "PASS_TURN_TOOL",
308
+ "request_pass",
306
309
  "ToolContextPolicy",
307
310
  "ToolContextConfig",
308
311
  "__version__",
@@ -204,6 +204,10 @@ class LLMTokenUsage:
204
204
  accepted_tokens: int | None = None
205
205
  rejected_tokens: int | None = None
206
206
 
207
+ #: Charge the provider itself reported for this call (e.g. OpenRouter's ``usage.cost``), in the
208
+ #: provider's billing currency. ``None`` = not reported; hosts then price tokens themselves.
209
+ cost: float | None = None
210
+
207
211
  def as_dict(self) -> dict[str, int | None]:
208
212
  return dict(self.__dict__)
209
213
 
@@ -419,6 +419,14 @@ class OpenAICompatibleChatLLMService(LLMService):
419
419
  return raw
420
420
 
421
421
  def _usage_obj(self, usage: Any = None) -> LLMTokenUsage:
422
+ def _cost_or_none(v: Any) -> float | None:
423
+ if isinstance(v, bool) or not isinstance(v, (int, float, str)):
424
+ return None
425
+ try:
426
+ return float(v)
427
+ except ValueError:
428
+ return None
429
+
422
430
  d = self._last_usage if usage is None else self._usage_dict_from_any(usage)
423
431
 
424
432
  def _int_or_none(v: Any) -> int | None:
@@ -448,6 +456,7 @@ class OpenAICompatibleChatLLMService(LLMService):
448
456
  reasoning_tokens=_int_or_none(d.get("reasoning_tokens")),
449
457
  accepted_tokens=_int_or_none(d.get("accepted_tokens")),
450
458
  rejected_tokens=_int_or_none(d.get("rejected_tokens")),
459
+ cost=_cost_or_none(d.get("cost")),
451
460
  )
452
461
 
453
462
  async def _with_retries(self, fn, *, method: str) -> Any:
@@ -64,6 +64,7 @@ from power_loop.core.agent_context import (
64
64
  )
65
65
  from power_loop.core.events import AgentEventBus
66
66
  from power_loop.core.hooks import AgentHooks
67
+ from power_loop.core.pass_turn import ensure_pass_turn
67
68
  from power_loop.core.pipeline import (
68
69
  AgentPipeline,
69
70
  _tool_call_args,
@@ -132,6 +133,12 @@ class StatefulResult:
132
133
  usage: dict[str, int] = field(default_factory=dict)
133
134
  #: Tool invocations executed during this send.
134
135
  tool_calls: int = 0
136
+ #: design/188:调 pass_turn 明确结束的、理由;停的那一刻有没有刚折进来的新输入。
137
+ passed: bool = False
138
+ pass_reason: str | None = None
139
+ new_input_after_pass: bool = False
140
+ #: 每一步写的文字:[{step, text, tools}],按顺序(不只是最后一段)。
141
+ texts: list[dict[str, Any]] = field(default_factory=list)
135
142
 
136
143
 
137
144
  class _SyncLoopRunner:
@@ -1415,7 +1422,8 @@ class StatefulAgentLoop:
1415
1422
  base,
1416
1423
  inject_tool_descriptions=self.config.inject_tool_descriptions,
1417
1424
  tool_catalog_header=self.config.tool_catalog_header,
1418
- tool_registry=self.tool_registry,
1425
+ # pipeline 会补上永远在的 pass_turn(design/188):预览也补,才和模型看到的一样
1426
+ tool_registry=ensure_pass_turn(self.tool_registry) if self.tool_registry is not None else None,
1419
1427
  skills_dir=self.config.skills_dir,
1420
1428
  )
1421
1429
 
@@ -2232,6 +2240,10 @@ class StatefulAgentLoop:
2232
2240
  pending_interactions=result.pending_interactions,
2233
2241
  usage=result.usage,
2234
2242
  tool_calls=result.tool_calls,
2243
+ passed=result.passed,
2244
+ pass_reason=result.pass_reason,
2245
+ new_input_after_pass=result.new_input_after_pass,
2246
+ texts=result.texts,
2235
2247
  )
2236
2248
 
2237
2249
  async def _migrate_prior_history_to_projection(
@@ -464,3 +464,9 @@ class AgentLoopResult:
464
464
  usage: dict[str, int] = field(default_factory=dict)
465
465
  #: Tool invocations executed during this run.
466
466
  tool_calls: int = 0
467
+ #: design/188:这一轮是不是调 pass_turn 明确结束的、理由;停的那一刻是否有刚折进来的新输入。
468
+ passed: bool = False
469
+ pass_reason: str | None = None
470
+ new_input_after_pass: bool = False
471
+ #: 每一步写的文字(不只是最后一段):[{step, text, tools}],按顺序。
472
+ texts: list[dict[str, Any]] = field(default_factory=list)
@@ -80,6 +80,13 @@ class LlmCallCompletedPayload(BaseEventPayload):
80
80
  #: ``ok`` · ``error`` · ``timeout`` · ``aborted`` (the attempt's task was cancelled: a retry
81
81
  #: deadline, steering, a stop).
82
82
  outcome: str = "ok"
83
+ #: 7.2: which endpoint served the call (the transport's base URL) — hosts price per connection,
84
+ #: and one model name can be served by several providers at different prices.
85
+ endpoint: str = ""
86
+ #: 7.2: charge the provider reported for this call (``LLMTokenUsage.cost``), ``None`` if not reported.
87
+ provider_cost: float | None = None
88
+ #: 7.2: image input tokens, when the provider splits them out (``None`` = not reported).
89
+ prompt_image_tokens: int | None = None
83
90
 
84
91
 
85
92
  @dataclass
@@ -0,0 +1,82 @@
1
+ """pass_turn —— 内置的「这一步做完了,结束这一轮」(design/188)。
2
+
3
+ 一个 send 有两种正常的结束:这一轮只写字、不调工具(自然结束);或者调 ``pass_turn``(明确结束)。
4
+ 不强制以 pass_turn 结尾。
5
+
6
+ - 工具**永远在**:pipeline 建立时 ``ensure_pass_turn`` 把它补进工具表(宿主去不掉)。
7
+ - 宿主可以用同名工具覆盖说明和处理(例如先核对再决定停不停);处理里调 ``request_pass(理由)`` 才算数。
8
+ - 硬停:标记之后,pipeline 在**下一轮开始前**停(不再调模型),结果上 ``passed=True``、``pass_reason``;
9
+ 停的时候若有刚折进来的新输入(转向消息),记 ``new_input_after_pass``,由宿主决定要不要另起一轮。
10
+ - 状态按 send 隔离(ContextVar,每个 pipeline.run 开始时装一份、结束时还原):子 agent 在同一个任务里
11
+ 内联跑,它的 pass 不会翻父 send 的标记。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from contextvars import ContextVar, Token
16
+ from typing import Any
17
+
18
+ from power_loop.contracts.tools import ToolDefinition
19
+
20
+ NAME = "pass_turn"
21
+ REASON_MAX = 300
22
+
23
+ DESCRIPTION = (
24
+ "这一步的事做完了,结束这一轮(不再继续调用模型)。"
25
+ "什么时候用:要做的已经做完、结果已经交出去(写出来了或用工具交了),没有别的要做;"
26
+ "或者在等后台的结果(它好了会叫醒你)。"
27
+ "只写字、不调工具也会结束这一轮——用它是明确地说「做完了」。reason 写一句为什么结束。"
28
+ )
29
+
30
+ PASS_TURN_TOOL = ToolDefinition(
31
+ name=NAME,
32
+ description=DESCRIPTION,
33
+ input_schema={
34
+ "type": "object",
35
+ "properties": {"reason": {"type": "string", "description": "一句话:为什么结束这一轮(记日志,不展示)。"}},
36
+ "required": ["reason"],
37
+ },
38
+ required_params=("reason",),
39
+ )
40
+
41
+ _state: ContextVar[dict[str, Any] | None] = ContextVar("power_loop_pass_turn", default=None)
42
+
43
+
44
+ def begin() -> Token:
45
+ """一个 send 开始:装一份新的状态。返回还原用的 token。"""
46
+ return _state.set({"passed": False, "reason": None, "new_input_after_pass": False})
47
+
48
+
49
+ def end(token: Token) -> None:
50
+ _state.reset(token)
51
+
52
+
53
+ def current() -> dict[str, Any] | None:
54
+ return _state.get()
55
+
56
+
57
+ def request_pass(reason: str = "") -> bool:
58
+ """标记「这一轮结束」:下一轮开始前停。不在 send 里(没有状态)返回 False。"""
59
+ st = _state.get()
60
+ if st is None:
61
+ return False
62
+ st["passed"] = True
63
+ st["reason"] = (reason or "").strip()[:REASON_MAX]
64
+ return True
65
+
66
+
67
+ def pass_requested() -> bool:
68
+ st = _state.get()
69
+ return bool(st and st.get("passed"))
70
+
71
+
72
+ def default_handler(reason: str = "") -> str:
73
+ if not request_pass(reason):
74
+ return "pass_turn 记下了(当前没有在跑的一轮)。"
75
+ return "这一轮结束了。不用再写别的。"
76
+
77
+
78
+ def ensure_pass_turn(registry: Any) -> Any:
79
+ """工具表里没有 pass_turn 就补上(永远在)。"""
80
+ if not registry.has(NAME):
81
+ registry.register(PASS_TURN_TOOL, default_handler)
82
+ return registry
@@ -202,6 +202,19 @@ def _maybe_compact_accepts_context(func: Any) -> bool:
202
202
 
203
203
  # ── Utility functions (unchanged from old agent.py) ──
204
204
 
205
+ def _llm_endpoint(llm: Any) -> str:
206
+ """The base URL that serves this LLM (unwrapping retry / proxy wrappers), or ""."""
207
+ for _ in range(5):
208
+ if llm is None:
209
+ break
210
+ cfg = getattr(llm, "_cfg", None) or getattr(llm, "config", None)
211
+ url = getattr(cfg, "base_url", None)
212
+ if url:
213
+ return str(url)
214
+ llm = getattr(llm, "inner", None) or getattr(llm, "_inner", None) or getattr(llm, "llm", None)
215
+ return ""
216
+
217
+
205
218
  def _truncate_result(output: Any, limit: int | None = RESULT_MAX_CHARS) -> str:
206
219
  """Cap a tool result before it enters history and the audit log (None = no cap). Keeps the
207
220
  head and the tail: errors and summaries are usually at the end."""
@@ -340,7 +353,10 @@ class AgentPipeline:
340
353
  ) -> None:
341
354
  self.llm = llm
342
355
  self.config = config
343
- self.tool_registry = tool_registry
356
+ # design/188:pass_turn 永远在(宿主可以用同名工具覆盖说明和处理)。
357
+ # 没有工具表 = 工具全由宿主执行(调用原样交回,pending_tools):那种模式不补,宿主自己处理
358
+ from power_loop.core.pass_turn import ensure_pass_turn
359
+ self.tool_registry = ensure_pass_turn(tool_registry) if tool_registry is not None else None
344
360
  self.hooks = hooks
345
361
  self.bus = bus
346
362
  self.ctx = ctx
@@ -832,6 +848,11 @@ class AgentPipeline:
832
848
  pending_interactions: list | None = None) -> AgentLoopResult:
833
849
  self._completed_rounds = rounds # for MemorySnapshot
834
850
  self._log_send_summary(status, rounds)
851
+ st = getattr(self, "_pass_state", None) or {}
852
+ texts = list(getattr(self, "_step_texts", []) or [])
853
+ # 明确结束(pass_turn)时最后一轮往往没有文字:答案取最后一段写过的文字
854
+ if st.get("passed") and not final_text and texts:
855
+ final_text = texts[-1]["text"]
835
856
  return AgentLoopResult(
836
857
  status=status, # type: ignore[arg-type]
837
858
  final_text=final_text,
@@ -841,6 +862,10 @@ class AgentPipeline:
841
862
  messages=self.history,
842
863
  usage=dict(self.ctx.usage_totals),
843
864
  tool_calls=int(self.ctx.tool_calls),
865
+ passed=bool(st.get("passed")),
866
+ pass_reason=st.get("reason"),
867
+ new_input_after_pass=bool(st.get("new_input_after_pass")),
868
+ texts=texts,
844
869
  )
845
870
 
846
871
  async def _persist_pending_interaction(
@@ -1333,6 +1358,7 @@ class AgentPipeline:
1333
1358
  success=False, error_type=type(exc).__name__,
1334
1359
  prompt_tokens=p_est, completion_tokens=c_est,
1335
1360
  total_tokens=p_est + c_est, estimated=True, outcome=outcome,
1361
+ endpoint=_llm_endpoint(self.llm),
1336
1362
  ),
1337
1363
  round_index=round_index, stream_id="main",
1338
1364
  )
@@ -1364,6 +1390,9 @@ class AgentPipeline:
1364
1390
  prompt_cached_tokens=getattr(usage, "prompt_cached_tokens", None),
1365
1391
  prompt_cache_miss_tokens=getattr(usage, "prompt_cache_miss_tokens", None),
1366
1392
  estimated=estimated,
1393
+ endpoint=_llm_endpoint(self.llm),
1394
+ provider_cost=getattr(usage, "cost", None),
1395
+ prompt_image_tokens=getattr(usage, "prompt_image_tokens", None),
1367
1396
  ),
1368
1397
  round_index=round_index, stream_id="main",
1369
1398
  )
@@ -1499,6 +1528,17 @@ class AgentPipeline:
1499
1528
 
1500
1529
  async def run(self, messages: list[LoopMessage]) -> AgentLoopResult:
1501
1530
  """Run the full agent loop. Returns when done, cancelled, or hit round limit."""
1531
+ from power_loop.core import pass_turn as _pass
1532
+
1533
+ token = _pass.begin() # pass_turn 的状态按 send 隔离(子 agent 内联跑也不串)
1534
+ self._pass_state = _pass.current()
1535
+ self._step_texts: list[dict[str, Any]] = []
1536
+ try:
1537
+ return await self._run(messages)
1538
+ finally:
1539
+ _pass.end(token)
1540
+
1541
+ async def _run(self, messages: list[LoopMessage]) -> AgentLoopResult:
1502
1542
  self.history = [dict(m) for m in messages]
1503
1543
  self._image_rounds = {} # 6.12.0:本 send 新入的图片行 id(row) → round_index
1504
1544
  self._tok_len = -1 # SCALE-4: wholesale (re)assignment invalidates the estimate
@@ -1646,6 +1686,14 @@ class AgentPipeline:
1646
1686
  for msg in drained:
1647
1687
  await self._append_message(msg, round_index=round_idx)
1648
1688
 
1689
+ # ── pass_turn:上一轮调了 → 这一轮开始前硬停(design/188,不再调模型)──
1690
+ if self._pass_state and self._pass_state.get("passed"):
1691
+ if drained_count:
1692
+ # 停的这一刻有刚折进来的新输入:不撤销停,留痕交给宿主(另起一轮处理)
1693
+ self._pass_state["new_input_after_pass"] = True
1694
+ await self._finalize("pass_turn")
1695
+ return self._make_result("completed", rounds=round_idx)
1696
+
1649
1697
  # ── Hook: ROUND_START ──
1650
1698
  round_ctx = RoundStartCtx(
1651
1699
  round_index=round_idx, messages=self.history,
@@ -1854,6 +1902,11 @@ class AgentPipeline:
1854
1902
  tool_calls = response.get_tool_calls()
1855
1903
  self._emit(AgentEventType.ROUND_TOOLS_PRESENT, RoundToolsPresentPayload(has_tools=bool(tool_calls)), round_index=round_idx)
1856
1904
 
1905
+ # 每一步的文字(design/188):模型每一轮都可能写字,全部留着交给宿主(不只是最后一段)
1906
+ if assistant_text:
1907
+ self._step_texts.append({"step": round_idx + 1, "text": assistant_text,
1908
+ "tools": [_tool_call_name(tc) for tc in (tool_calls or [])]})
1909
+
1857
1910
  # Any productive round (said something OR called a tool) clears the empty streak.
1858
1911
  if assistant_text or tool_calls:
1859
1912
  self._empty_response_streak = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 7.1.0
3
+ Version: 7.3.0
4
4
  Summary: Embeddable agent execution kernel — LLM loop, hooks, events, tools, dynamic sub-agents.
5
5
  Author-email: zhangran <zhangran24@126.com>
6
6
  License: MIT
@@ -46,6 +46,7 @@ power_loop/contrib/otel_sink.py
46
46
  power_loop/core/agent_context.py
47
47
  power_loop/core/events.py
48
48
  power_loop/core/hooks.py
49
+ power_loop/core/pass_turn.py
49
50
  power_loop/core/phase.py
50
51
  power_loop/core/pipeline.py
51
52
  power_loop/core/runner.py
File without changes
File without changes
File without changes
File without changes