power-loop 7.2.0__tar.gz → 7.3.1__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.2.0 → power_loop-7.3.1}/PKG-INFO +1 -1
  2. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/__init__.py +5 -1
  3. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/agent/stateful_loop.py +13 -1
  4. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/agent/types.py +6 -0
  5. power_loop-7.3.1/power_loop/core/pass_turn.py +101 -0
  6. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/core/pipeline.py +39 -1
  7. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop.egg-info/PKG-INFO +1 -1
  8. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop.egg-info/SOURCES.txt +1 -0
  9. {power_loop-7.2.0 → power_loop-7.3.1}/LICENSE +0 -0
  10. {power_loop-7.2.0 → power_loop-7.3.1}/README.md +0 -0
  11. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/__init__.py +0 -0
  12. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/__init__.py +0 -0
  13. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
  14. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/attempt_scope.py +0 -0
  15. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/capabilities.py +0 -0
  16. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/interface.py +0 -0
  17. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/llm_factory.py +0 -0
  18. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
  19. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
  20. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/multimodal.py +0 -0
  21. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/_vendor/llm_client/proxy_fallback.py +0 -0
  22. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/agent/__init__.py +0 -0
  23. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/agent/follow_up.py +0 -0
  24. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/agent/sink.py +0 -0
  25. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/agent/system_prompt.py +0 -0
  26. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/__init__.py +0 -0
  27. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/context_policy.py +0 -0
  28. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/errors.py +0 -0
  29. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/event_payloads.py +0 -0
  30. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/events.py +0 -0
  31. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/handlers.py +0 -0
  32. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/hook_contexts.py +0 -0
  33. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/hooks.py +0 -0
  34. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/messages.py +0 -0
  35. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/protocols.py +0 -0
  36. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contracts/tools.py +0 -0
  37. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contrib/__init__.py +0 -0
  38. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contrib/_redact.py +0 -0
  39. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contrib/jsonl_sink.py +0 -0
  40. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contrib/logging_sink.py +0 -0
  41. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contrib/mcp.py +0 -0
  42. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contrib/metrics_sink.py +0 -0
  43. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/contrib/otel_sink.py +0 -0
  44. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/core/agent_context.py +0 -0
  45. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/core/events.py +0 -0
  46. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/core/hooks.py +0 -0
  47. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/core/phase.py +0 -0
  48. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/core/runner.py +0 -0
  49. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/core/state.py +0 -0
  50. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/py.typed +0 -0
  51. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/background.py +0 -0
  52. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/bash_capture.py +0 -0
  53. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/blackboard.py +0 -0
  54. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/budget.py +0 -0
  55. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/cancellation.py +0 -0
  56. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/capability_probe.py +0 -0
  57. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/compact.py +0 -0
  58. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/context_worker.py +0 -0
  59. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/env.py +0 -0
  60. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/exec_backend.py +0 -0
  61. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/fold.py +0 -0
  62. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/fold_adapter.py +0 -0
  63. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/history_projector.py +0 -0
  64. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/history_sanitize.py +0 -0
  65. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/human_input.py +0 -0
  66. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/image_recall.py +0 -0
  67. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/lineage.py +0 -0
  68. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/memory.py +0 -0
  69. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/notes.py +0 -0
  70. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/provider.py +0 -0
  71. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/representation.py +0 -0
  72. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/retry.py +0 -0
  73. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/runtime_state.py +0 -0
  74. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/session_store.py +0 -0
  75. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/skills.py +0 -0
  76. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/spec.py +0 -0
  77. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/steer.py +0 -0
  78. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/stop_policy.py +0 -0
  79. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/__init__.py +0 -0
  80. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/backends/__init__.py +0 -0
  81. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/backends/mysql.py +0 -0
  82. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/backends/postgres.py +0 -0
  83. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/backends/sqlite.py +0 -0
  84. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/capabilities.py +0 -0
  85. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/context_cache.py +0 -0
  86. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/db.py +0 -0
  87. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/dialect.py +0 -0
  88. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/factory.py +0 -0
  89. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/schema.py +0 -0
  90. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/store.py +0 -0
  91. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/store/types.py +0 -0
  92. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/structured.py +0 -0
  93. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/stub_provider.py +0 -0
  94. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/timers.py +0 -0
  95. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/todos.py +0 -0
  96. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/tool_archive.py +0 -0
  97. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/tool_context.py +0 -0
  98. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/tool_truncation.py +0 -0
  99. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/runtime/usage_estimate.py +0 -0
  100. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/tools/__init__.py +0 -0
  101. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/tools/blackboard.py +0 -0
  102. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/tools/command_policy.py +0 -0
  103. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/tools/command_rules.py +0 -0
  104. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/tools/default_manifest.py +0 -0
  105. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/tools/default_tools.py +0 -0
  106. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/tools/registry.py +0 -0
  107. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/tools/spawn_agent.py +0 -0
  108. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/__init__.py +0 -0
  109. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/admission.py +0 -0
  110. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/api.py +0 -0
  111. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/engine.py +0 -0
  112. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/introspect.py +0 -0
  113. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/journal.py +0 -0
  114. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/result.py +0 -0
  115. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/resume.py +0 -0
  116. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/runner.py +0 -0
  117. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/spec.py +0 -0
  118. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/subprocess_executor.py +0 -0
  119. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/tool.py +0 -0
  120. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop/workflow/worker.py +0 -0
  121. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop.egg-info/dependency_links.txt +0 -0
  122. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop.egg-info/requires.txt +0 -0
  123. {power_loop-7.2.0 → power_loop-7.3.1}/power_loop.egg-info/top_level.txt +0 -0
  124. {power_loop-7.2.0 → power_loop-7.3.1}/pyproject.toml +0 -0
  125. {power_loop-7.2.0 → power_loop-7.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 7.2.0
3
+ Version: 7.3.1
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.2.0"
18
+ __version__ = "7.3.1"
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, pass_progress, 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,9 @@ STABLE_API = (
303
304
  )
304
305
 
305
306
  __all__ = [
307
+ "PASS_TURN_TOOL",
308
+ "request_pass",
309
+ "pass_progress",
306
310
  "ToolContextPolicy",
307
311
  "ToolContextConfig",
308
312
  "__version__",
@@ -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)
@@ -0,0 +1,101 @@
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, "texts": 0, "work_tools": 0})
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 note_step(wrote_text: bool, tool_names: list[str]) -> None:
68
+ """pipeline 每一轮调一次:记下这次 send 写过几段字、调过几次别的工具(pass_turn 不算)。"""
69
+ st = _state.get()
70
+ if st is None:
71
+ return
72
+ if wrote_text:
73
+ st["texts"] = st.get("texts", 0) + 1
74
+ st["work_tools"] = st.get("work_tools", 0) + sum(1 for n in tool_names if n != PASS_TURN_TOOL.name)
75
+
76
+
77
+ def pass_progress() -> dict[str, int] | None:
78
+ """这次 send 到现在:写过几段字(texts)、调过几次别的工具(work_tools)。宿主覆盖 pass_turn 时用来判断
79
+ 「还什么都没交就要结束」。不在 send 里返回 None。"""
80
+ st = _state.get()
81
+ if st is None:
82
+ return None
83
+ return {"texts": int(st.get("texts", 0)), "work_tools": int(st.get("work_tools", 0))}
84
+
85
+
86
+ def pass_requested() -> bool:
87
+ st = _state.get()
88
+ return bool(st and st.get("passed"))
89
+
90
+
91
+ def default_handler(reason: str = "") -> str:
92
+ if not request_pass(reason):
93
+ return "pass_turn 记下了(当前没有在跑的一轮)。"
94
+ return "这一轮结束了。不用再写别的。"
95
+
96
+
97
+ def ensure_pass_turn(registry: Any) -> Any:
98
+ """工具表里没有 pass_turn 就补上(永远在)。"""
99
+ if not registry.has(NAME):
100
+ registry.register(PASS_TURN_TOOL, default_handler)
101
+ return registry
@@ -353,7 +353,10 @@ class AgentPipeline:
353
353
  ) -> None:
354
354
  self.llm = llm
355
355
  self.config = config
356
- 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
357
360
  self.hooks = hooks
358
361
  self.bus = bus
359
362
  self.ctx = ctx
@@ -845,6 +848,11 @@ class AgentPipeline:
845
848
  pending_interactions: list | None = None) -> AgentLoopResult:
846
849
  self._completed_rounds = rounds # for MemorySnapshot
847
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"]
848
856
  return AgentLoopResult(
849
857
  status=status, # type: ignore[arg-type]
850
858
  final_text=final_text,
@@ -854,6 +862,10 @@ class AgentPipeline:
854
862
  messages=self.history,
855
863
  usage=dict(self.ctx.usage_totals),
856
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,
857
869
  )
858
870
 
859
871
  async def _persist_pending_interaction(
@@ -1516,6 +1528,17 @@ class AgentPipeline:
1516
1528
 
1517
1529
  async def run(self, messages: list[LoopMessage]) -> AgentLoopResult:
1518
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:
1519
1542
  self.history = [dict(m) for m in messages]
1520
1543
  self._image_rounds = {} # 6.12.0:本 send 新入的图片行 id(row) → round_index
1521
1544
  self._tok_len = -1 # SCALE-4: wholesale (re)assignment invalidates the estimate
@@ -1663,6 +1686,14 @@ class AgentPipeline:
1663
1686
  for msg in drained:
1664
1687
  await self._append_message(msg, round_index=round_idx)
1665
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
+
1666
1697
  # ── Hook: ROUND_START ──
1667
1698
  round_ctx = RoundStartCtx(
1668
1699
  round_index=round_idx, messages=self.history,
@@ -1871,6 +1902,13 @@ class AgentPipeline:
1871
1902
  tool_calls = response.get_tool_calls()
1872
1903
  self._emit(AgentEventType.ROUND_TOOLS_PRESENT, RoundToolsPresentPayload(has_tools=bool(tool_calls)), round_index=round_idx)
1873
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
+ from power_loop.core import pass_turn as _pass_state
1910
+ _pass_state.note_step(bool(assistant_text), [_tool_call_name(tc) for tc in (tool_calls or [])])
1911
+
1874
1912
  # Any productive round (said something OR called a tool) clears the empty streak.
1875
1913
  if assistant_text or tool_calls:
1876
1914
  self._empty_response_streak = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 7.2.0
3
+ Version: 7.3.1
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