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.
- {power_loop-5.2.0 → power_loop-5.2.2}/PKG-INFO +1 -1
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/__init__.py +1 -1
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/pipeline.py +7 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/engine.py +15 -7
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/spec.py +20 -2
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/PKG-INFO +1 -1
- {power_loop-5.2.0 → power_loop-5.2.2}/LICENSE +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/README.md +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/__init__.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/__init__.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/capabilities.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/interface.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/llm_factory.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/_vendor/llm_client/multimodal.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/__init__.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/follow_up.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/sink.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/stateful_loop.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/system_prompt.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/agent/types.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/__init__.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/errors.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/event_payloads.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/events.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/handlers.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/hook_contexts.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/hooks.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/messages.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/protocols.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contracts/tools.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/__init__.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/_redact.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/jsonl_sink.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/logging_sink.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/mcp.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/metrics_sink.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/contrib/otel_sink.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/agent_context.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/events.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/hooks.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/phase.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/runner.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/core/state.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/py.typed +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/blackboard.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/budget.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/cancellation.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/compact.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/env.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/exec_backend.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/fold.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/fold_adapter.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/history_projector.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/history_sanitize.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/human_input.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/memory.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/notes.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/provider.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/representation.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/retry.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/runtime_state.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/session_store.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/skills.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/spec.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/__init__.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/backends/__init__.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/backends/mysql.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/backends/postgres.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/backends/sqlite.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/capabilities.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/db.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/dialect.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/factory.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/schema.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/store.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/store/types.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/structured.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/stub_provider.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/runtime/timers.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/__init__.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/blackboard.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/default_manifest.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/default_tools.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/registry.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/tools/spawn_agent.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/__init__.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/api.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/introspect.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/journal.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/result.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/resume.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/runner.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/subprocess_executor.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/tool.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop/workflow/worker.py +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/SOURCES.txt +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/dependency_links.txt +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/requires.txt +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/power_loop.egg-info/top_level.txt +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/pyproject.toml +0 -0
- {power_loop-5.2.0 → power_loop-5.2.2}/setup.cfg +0 -0
|
@@ -15,7 +15,7 @@ Stability tiers
|
|
|
15
15
|
无版本承诺,可随时变更或删除。
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
__version__ = "5.2.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|