power-loop 5.2.0__tar.gz → 5.2.2__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 (104) hide show
  1. {power_loop-5.2.0 → power_loop-5.2.2}/PKG-INFO +1 -1
  2. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/__init__.py +1 -1
  3. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/pipeline.py +7 -0
  4. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/engine.py +15 -7
  5. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/spec.py +20 -2
  6. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/PKG-INFO +1 -1
  7. {power_loop-5.2.0 → power_loop-5.2.2}/LICENSE +0 -0
  8. {power_loop-5.2.0 → power_loop-5.2.2}/README.md +0 -0
  9. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/__init__.py +0 -0
  10. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/__init__.py +0 -0
  11. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
  12. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/capabilities.py +0 -0
  13. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/interface.py +0 -0
  14. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/llm_factory.py +0 -0
  15. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
  16. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
  17. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/multimodal.py +0 -0
  18. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/__init__.py +0 -0
  19. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/follow_up.py +0 -0
  20. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/sink.py +0 -0
  21. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/stateful_loop.py +0 -0
  22. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/system_prompt.py +0 -0
  23. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/types.py +0 -0
  24. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/__init__.py +0 -0
  25. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/errors.py +0 -0
  26. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/event_payloads.py +0 -0
  27. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/events.py +0 -0
  28. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/handlers.py +0 -0
  29. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/hook_contexts.py +0 -0
  30. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/hooks.py +0 -0
  31. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/messages.py +0 -0
  32. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/protocols.py +0 -0
  33. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/tools.py +0 -0
  34. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/__init__.py +0 -0
  35. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/_redact.py +0 -0
  36. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/jsonl_sink.py +0 -0
  37. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/logging_sink.py +0 -0
  38. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/mcp.py +0 -0
  39. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/metrics_sink.py +0 -0
  40. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/otel_sink.py +0 -0
  41. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/agent_context.py +0 -0
  42. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/events.py +0 -0
  43. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/hooks.py +0 -0
  44. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/phase.py +0 -0
  45. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/runner.py +0 -0
  46. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/state.py +0 -0
  47. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/py.typed +0 -0
  48. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/blackboard.py +0 -0
  49. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/budget.py +0 -0
  50. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/cancellation.py +0 -0
  51. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/compact.py +0 -0
  52. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/env.py +0 -0
  53. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/exec_backend.py +0 -0
  54. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/fold.py +0 -0
  55. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/fold_adapter.py +0 -0
  56. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/history_projector.py +0 -0
  57. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/history_sanitize.py +0 -0
  58. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/human_input.py +0 -0
  59. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/memory.py +0 -0
  60. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/notes.py +0 -0
  61. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/provider.py +0 -0
  62. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/representation.py +0 -0
  63. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/retry.py +0 -0
  64. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/runtime_state.py +0 -0
  65. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/session_store.py +0 -0
  66. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/skills.py +0 -0
  67. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/spec.py +0 -0
  68. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/__init__.py +0 -0
  69. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/backends/__init__.py +0 -0
  70. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/backends/mysql.py +0 -0
  71. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/backends/postgres.py +0 -0
  72. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/backends/sqlite.py +0 -0
  73. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/capabilities.py +0 -0
  74. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/db.py +0 -0
  75. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/dialect.py +0 -0
  76. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/factory.py +0 -0
  77. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/schema.py +0 -0
  78. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/store.py +0 -0
  79. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/types.py +0 -0
  80. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/structured.py +0 -0
  81. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/stub_provider.py +0 -0
  82. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/timers.py +0 -0
  83. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/__init__.py +0 -0
  84. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/blackboard.py +0 -0
  85. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/default_manifest.py +0 -0
  86. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/default_tools.py +0 -0
  87. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/registry.py +0 -0
  88. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/spawn_agent.py +0 -0
  89. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/__init__.py +0 -0
  90. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/api.py +0 -0
  91. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/introspect.py +0 -0
  92. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/journal.py +0 -0
  93. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/result.py +0 -0
  94. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/resume.py +0 -0
  95. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/runner.py +0 -0
  96. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/subprocess_executor.py +0 -0
  97. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/tool.py +0 -0
  98. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/worker.py +0 -0
  99. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/SOURCES.txt +0 -0
  100. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/dependency_links.txt +0 -0
  101. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/requires.txt +0 -0
  102. {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/top_level.txt +0 -0
  103. {power_loop-5.2.0 → power_loop-5.2.2}/pyproject.toml +0 -0
  104. {power_loop-5.2.0 → power_loop-5.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 5.2.0
3
+ Version: 5.2.2
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__ = "5.2.0"
18
+ __version__ = "5.2.2"
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).
@@ -1420,6 +1420,13 @@ class AgentPipeline:
1420
1420
  used_todo=used_todo,
1421
1421
  )
1422
1422
  await self.hooks.run_typed_async(HookPoint.ROUND_END, round_end)
1423
+ # Per-round usage row for TOOL rounds too. This was only emitted on the
1424
+ # no-tools paths above, so an agent session (where nearly every round calls
1425
+ # tools) persisted usage for just its final round — a 34-round leaf showed
1426
+ # exactly one usage_rounds row. Totals were never wrong (session_stats bumps
1427
+ # from the run's in-memory aggregate at end-of-send); what was missing is the
1428
+ # per-round breakdown this table exists for.
1429
+ await self._emit_sink(self.sink.on_round_ended, round_idx, usage=usage)
1423
1430
 
1424
1431
  if not used_todo:
1425
1432
  self.rounds_since_todo += 1
@@ -42,6 +42,7 @@ from power_loop.runtime.structured import StructuredOutputError, parse_structure
42
42
 
43
43
  from .result import AgentResult, SharedBudget, WorkflowResult
44
44
  from .spec import (
45
+ MAX_BACKOFF_S,
45
46
  MAX_FOREACH_ITEMS,
46
47
  AgentNode,
47
48
  BranchNode,
@@ -152,6 +153,9 @@ async def _usage_for(parent_loop: Any, session_id: str | None) -> dict[str, int]
152
153
  #: :class:`WorkflowFileIO` 实现——那部分带语言和产品口径,不该焊进库里。
153
154
  FILEREF_RE = re.compile(r"@@FILEREF:([^\[\]@]+)(\[[^\]]*\])?@@")
154
155
 
156
+ #: foreach 把当前迭代序号放进 body 的 env(保留键),供 FileIO 端口分配 per-iteration 产出文件。
157
+ ITERATION_ENV_KEY = "__iteration__"
158
+
155
159
 
156
160
  class WorkflowFileIO(Protocol):
157
161
  """叶子产出文件与文件引用的宿主端口(design/64 §2)。引擎自己不碰文件系统。"""
@@ -392,7 +396,7 @@ class WorkflowEngine:
392
396
  if self._file_io is not None:
393
397
  try:
394
398
  output_file = node.output_file or self._file_io.output_path(
395
- node.id, iteration=env.get("__iteration__")
399
+ node.id, iteration=env.get(ITERATION_ENV_KEY)
396
400
  )
397
401
  except Exception: # noqa: BLE001 — 产出文件是增益,不该让整个叶子起不来
398
402
  logger.debug("file_io.output_path failed for %s", node.id, exc_info=True)
@@ -448,7 +452,9 @@ class WorkflowEngine:
448
452
  except Exception: # noqa: BLE001 — 归档失败不该毁掉这次重试
449
453
  logger.debug("file_io.before_attempt failed", exc_info=True)
450
454
  if policy is not None and policy.backoff_s > 0:
451
- await asyncio.sleep(policy.backoff_s)
455
+ # 第 N 次重试等 backoff_s * factor**(N-1);factor=1 就是固定间隔。
456
+ delay = policy.backoff_s * (policy.backoff_factor ** (attempt - 2))
457
+ await asyncio.sleep(min(delay, MAX_BACKOFF_S))
452
458
  raw = await self._executor.run_agent(
453
459
  replace(spec, metadata={**spec.metadata, "attempt": attempt}),
454
460
  user_input, parent_loop=self._loop, driver_sid=driver_sid,
@@ -646,8 +652,10 @@ class WorkflowEngine:
646
652
  items = self._resolve_items(node)
647
653
  sem = asyncio.Semaphore(node.max_concurrency)
648
654
 
649
- async def one(item: Any) -> AgentResult | None:
650
- child_env = {**env, node.as_var: item}
655
+ async def one(item: Any, index: int) -> AgentResult | None:
656
+ # ITERATION_ENV_KEY:body 的所有迭代共享同一个 node_id,宿主的 FileIO 端口靠它
657
+ # 给每次迭代分不同的产出文件——不区分的话 N 个并发迭代会追加进同一个文件,互相搅乱。
658
+ child_env = {**env, node.as_var: item, ITERATION_ENV_KEY: index}
651
659
  if node.parallel:
652
660
  async with sem:
653
661
  return await self._exec(node.body, child_env, driver_sid)
@@ -659,13 +667,13 @@ class WorkflowEngine:
659
667
  try:
660
668
  if node.parallel:
661
669
  gathered = await self._gather_branches(
662
- [one(it) for it in items], on_error=node.on_error
670
+ [one(it, i) for i, it in enumerate(items)], on_error=node.on_error
663
671
  )
664
672
  else:
665
673
  gathered = []
666
- for it in items:
674
+ for i, it in enumerate(items):
667
675
  try:
668
- gathered.append(await one(it))
676
+ gathered.append(await one(it, i))
669
677
  except Exception as exc: # noqa: BLE001
670
678
  if node.on_error == "halt":
671
679
  raise
@@ -92,6 +92,9 @@ class WorkflowSpecError(SpecValidationError):
92
92
  #: ``empty`` = 完成了但没有正文(模型空转收尾)。
93
93
  RETRY_TRIGGERS = frozenset({"failed", "empty"})
94
94
  _MAX_ATTEMPTS_CAP = 5
95
+ _MAX_BACKOFF_FACTOR = 10.0
96
+ #: 单次重试等待的封顶秒数。
97
+ MAX_BACKOFF_S = 60.0
95
98
 
96
99
 
97
100
  @dataclass(frozen=True)
@@ -105,7 +108,11 @@ class RetryPolicy:
105
108
 
106
109
  max_attempts: int = 1
107
110
  on: tuple[str, ...] = ("failed", "empty")
111
+ #: 第一次重试前等多久。第 N 次重试等 ``backoff_s * backoff_factor**(N-1)``,
112
+ #: 单次等待封顶 :data:`MAX_BACKOFF_S`(等太久等于把整个 run 挂在那儿)。
108
113
  backoff_s: float = 0.0
114
+ #: 1.0 = 固定间隔;2.0 = 指数退避(2s → 4s → 8s)。provider 限流时才真正需要它。
115
+ backoff_factor: float = 1.0
109
116
 
110
117
 
111
118
  @dataclass(frozen=True)
@@ -346,6 +353,7 @@ def _node_to_dict(node: WorkflowNode) -> dict[str, Any]:
346
353
  "max_attempts": node.retry.max_attempts,
347
354
  "on": list(node.retry.on),
348
355
  "backoff_s": node.retry.backoff_s,
356
+ "backoff_factor": node.retry.backoff_factor,
349
357
  }
350
358
  if node.continue_on_error:
351
359
  out["continue_on_error"] = True
@@ -719,7 +727,7 @@ def _parse_retry(raw: Any, path: str, problems: list[str]) -> RetryPolicy | None
719
727
  if not isinstance(raw, dict):
720
728
  problems.append(f"{path}: 'retry' must be an object {{max_attempts, on?, backoff_s?}}")
721
729
  return None
722
- extra = set(raw) - {"max_attempts", "on", "backoff_s"}
730
+ extra = set(raw) - {"max_attempts", "on", "backoff_s", "backoff_factor"}
723
731
  if extra:
724
732
  problems.append(f"{path}.retry: unknown key(s): {sorted(extra)}")
725
733
  attempts = raw.get("max_attempts", 1)
@@ -748,7 +756,17 @@ def _parse_retry(raw: Any, path: str, problems: list[str]) -> RetryPolicy | None
748
756
  if isinstance(backoff, bool) or not isinstance(backoff, (int, float)) or backoff < 0:
749
757
  problems.append(f"{path}.retry: 'backoff_s' must be a number >= 0")
750
758
  backoff = 0.0
751
- return RetryPolicy(max_attempts=attempts, on=tuple(on_raw), backoff_s=float(backoff))
759
+ factor = raw.get("backoff_factor", 1.0)
760
+ if isinstance(factor, bool) or not isinstance(factor, (int, float)) or factor < 1.0:
761
+ problems.append(f"{path}.retry: 'backoff_factor' must be a number >= 1.0 (1.0 = fixed delay)")
762
+ factor = 1.0
763
+ elif factor > _MAX_BACKOFF_FACTOR:
764
+ problems.append(f"{path}.retry: 'backoff_factor' is capped at {_MAX_BACKOFF_FACTOR}")
765
+ factor = _MAX_BACKOFF_FACTOR
766
+ return RetryPolicy(
767
+ max_attempts=attempts, on=tuple(on_raw), backoff_s=float(backoff),
768
+ backoff_factor=float(factor),
769
+ )
752
770
 
753
771
 
754
772
  def _parse_sequence(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 5.2.0
3
+ Version: 5.2.2
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
File without changes
File without changes
File without changes
File without changes