power-loop 6.46.0__tar.gz → 6.46.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 (113) hide show
  1. {power_loop-6.46.0 → power_loop-6.46.2}/PKG-INFO +1 -1
  2. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/__init__.py +1 -1
  3. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/engine.py +51 -6
  4. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/spec.py +27 -2
  5. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop.egg-info/PKG-INFO +1 -1
  6. {power_loop-6.46.0 → power_loop-6.46.2}/LICENSE +0 -0
  7. {power_loop-6.46.0 → power_loop-6.46.2}/README.md +0 -0
  8. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/__init__.py +0 -0
  9. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/llm_client/__init__.py +0 -0
  10. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
  11. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/llm_client/capabilities.py +0 -0
  12. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/llm_client/interface.py +0 -0
  13. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/llm_client/llm_factory.py +0 -0
  14. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
  15. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
  16. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/llm_client/multimodal.py +0 -0
  17. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/_vendor/llm_client/proxy_fallback.py +0 -0
  18. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/agent/__init__.py +0 -0
  19. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/agent/follow_up.py +0 -0
  20. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/agent/sink.py +0 -0
  21. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/agent/stateful_loop.py +0 -0
  22. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/agent/system_prompt.py +0 -0
  23. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/agent/types.py +0 -0
  24. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/__init__.py +0 -0
  25. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/errors.py +0 -0
  26. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/event_payloads.py +0 -0
  27. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/events.py +0 -0
  28. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/handlers.py +0 -0
  29. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/hook_contexts.py +0 -0
  30. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/hooks.py +0 -0
  31. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/messages.py +0 -0
  32. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/protocols.py +0 -0
  33. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contracts/tools.py +0 -0
  34. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contrib/__init__.py +0 -0
  35. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contrib/_redact.py +0 -0
  36. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contrib/jsonl_sink.py +0 -0
  37. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contrib/logging_sink.py +0 -0
  38. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contrib/mcp.py +0 -0
  39. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contrib/metrics_sink.py +0 -0
  40. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/contrib/otel_sink.py +0 -0
  41. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/core/agent_context.py +0 -0
  42. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/core/events.py +0 -0
  43. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/core/hooks.py +0 -0
  44. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/core/phase.py +0 -0
  45. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/core/pipeline.py +0 -0
  46. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/core/runner.py +0 -0
  47. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/core/state.py +0 -0
  48. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/py.typed +0 -0
  49. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/blackboard.py +0 -0
  50. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/budget.py +0 -0
  51. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/cancellation.py +0 -0
  52. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/capability_probe.py +0 -0
  53. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/compact.py +0 -0
  54. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/env.py +0 -0
  55. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/exec_backend.py +0 -0
  56. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/fold.py +0 -0
  57. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/fold_adapter.py +0 -0
  58. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/history_projector.py +0 -0
  59. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/history_sanitize.py +0 -0
  60. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/human_input.py +0 -0
  61. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/image_recall.py +0 -0
  62. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/memory.py +0 -0
  63. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/notes.py +0 -0
  64. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/provider.py +0 -0
  65. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/representation.py +0 -0
  66. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/retry.py +0 -0
  67. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/runtime_state.py +0 -0
  68. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/session_store.py +0 -0
  69. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/skills.py +0 -0
  70. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/spec.py +0 -0
  71. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/steer.py +0 -0
  72. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/stop_policy.py +0 -0
  73. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/__init__.py +0 -0
  74. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/backends/__init__.py +0 -0
  75. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/backends/mysql.py +0 -0
  76. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/backends/postgres.py +0 -0
  77. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/backends/sqlite.py +0 -0
  78. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/capabilities.py +0 -0
  79. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/db.py +0 -0
  80. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/dialect.py +0 -0
  81. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/factory.py +0 -0
  82. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/schema.py +0 -0
  83. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/store.py +0 -0
  84. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/store/types.py +0 -0
  85. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/structured.py +0 -0
  86. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/stub_provider.py +0 -0
  87. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/timers.py +0 -0
  88. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/todos.py +0 -0
  89. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/runtime/usage_estimate.py +0 -0
  90. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/tools/__init__.py +0 -0
  91. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/tools/blackboard.py +0 -0
  92. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/tools/command_policy.py +0 -0
  93. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/tools/command_rules.py +0 -0
  94. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/tools/default_manifest.py +0 -0
  95. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/tools/default_tools.py +0 -0
  96. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/tools/registry.py +0 -0
  97. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/tools/spawn_agent.py +0 -0
  98. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/__init__.py +0 -0
  99. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/api.py +0 -0
  100. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/introspect.py +0 -0
  101. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/journal.py +0 -0
  102. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/result.py +0 -0
  103. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/resume.py +0 -0
  104. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/runner.py +0 -0
  105. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/subprocess_executor.py +0 -0
  106. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/tool.py +0 -0
  107. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop/workflow/worker.py +0 -0
  108. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop.egg-info/SOURCES.txt +0 -0
  109. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop.egg-info/dependency_links.txt +0 -0
  110. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop.egg-info/requires.txt +0 -0
  111. {power_loop-6.46.0 → power_loop-6.46.2}/power_loop.egg-info/top_level.txt +0 -0
  112. {power_loop-6.46.0 → power_loop-6.46.2}/pyproject.toml +0 -0
  113. {power_loop-6.46.0 → power_loop-6.46.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 6.46.0
3
+ Version: 6.46.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__ = "6.46.0"
18
+ __version__ = "6.46.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).
@@ -31,6 +31,7 @@ import asyncio
31
31
  import json
32
32
  import logging
33
33
  import re
34
+ from collections import ChainMap
34
35
  from collections.abc import Awaitable, Callable
35
36
  from contextvars import ContextVar
36
37
  from dataclasses import replace
@@ -58,6 +59,37 @@ from .spec import (
58
59
  #: 循环信息在 env 里的键(模板变量正则匹配不到它,不会被当成 {{变量}})
59
60
  _LOOP_KEY = "\x00loop"
60
61
 
62
+ #: 当前 foreach 迭代自己的结果表(6.46.1)。body 的节点在每次迭代里 id 都一样,并行迭代共用全局结果表时,
63
+ #: 体内的 inputs_from / when / 循环的 until 会读到别的迭代的结果。每个迭代在自己的 task 里开一层,嵌套的迭代再往里叠一层。
64
+ _ITER_SCOPE: ContextVar[ChainMap[str, AgentResult] | None] = ContextVar("pl_wf_iter_scope", default=None)
65
+
66
+
67
+ class _ScopedResults(dict):
68
+ """节点结果表:写的时候全局和当前迭代都写;读的时候先看当前迭代(再往外层迭代),最后看全局。
69
+ ``dict(self)`` / ``.items()`` 拿到的是全局那份(run 结果、journal 用)。"""
70
+
71
+ def __setitem__(self, key: str, value: AgentResult) -> None:
72
+ super().__setitem__(key, value)
73
+ scope = _ITER_SCOPE.get()
74
+ if scope is not None:
75
+ scope[key] = value
76
+
77
+ def get(self, key: str, default: Any = None) -> Any:
78
+ scope = _ITER_SCOPE.get()
79
+ if scope is not None and key in scope:
80
+ return scope[key]
81
+ return super().get(key, default)
82
+
83
+ def __getitem__(self, key: str) -> AgentResult:
84
+ scope = _ITER_SCOPE.get()
85
+ if scope is not None and key in scope:
86
+ return scope[key]
87
+ return super().__getitem__(key)
88
+
89
+ def __contains__(self, key: object) -> bool:
90
+ scope = _ITER_SCOPE.get()
91
+ return (scope is not None and key in scope) or super().__contains__(key)
92
+
61
93
  __all__ = ["Executor", "InProcessExecutor", "WorkflowEngine", "WorkflowRunError", "in_workflow"]
62
94
 
63
95
  logger = logging.getLogger(__name__)
@@ -329,7 +361,8 @@ class WorkflowEngine:
329
361
  # Stable id for this run; threaded into each leaf's metadata as an
330
362
  # idempotency key so side-effecting tools can dedupe across a resume.
331
363
  self._run_id = run_id
332
- self._results: dict[str, AgentResult] = {}
364
+ self._results: dict[str, AgentResult] = _ScopedResults()
365
+ self._iter_broken: list[str] = []
333
366
  self._errors: list[str] = []
334
367
  self._last: AgentResult | None = None
335
368
  self._budget_hit = False
@@ -403,10 +436,12 @@ class WorkflowEngine:
403
436
  nid for nid, r in self._results.items()
404
437
  if r.status not in ("completed", "skipped") and nid not in self._tolerated
405
438
  ]
439
+ # foreach 各路里没成功的(全局表里只留了最后写的那一路,见 _iter_broken)
440
+ broken += [b for b in self._iter_broken if b.rsplit(".", 1)[-1] not in self._tolerated]
406
441
  if broken:
407
442
  status = "failed"
408
443
  self._errors.append(
409
- "failed node(s): " + ", ".join(sorted(broken))
444
+ "failed node(s): " + ", ".join(sorted(set(broken)))
410
445
  + "(如果这些失败是可接受的,给对应节点加 continue_on_error: true)"
411
446
  )
412
447
  if self._leaf_cap_hit and status == "completed":
@@ -999,10 +1034,20 @@ class WorkflowEngine:
999
1034
  # ITERATION_ENV_KEY:body 的所有迭代共享同一个 node_id,宿主的 FileIO 端口靠它
1000
1035
  # 给每次迭代分不同的产出文件——不区分的话 N 个并发迭代会追加进同一个文件,互相搅乱。
1001
1036
  child_env = {**env, node.as_var: item, ITERATION_ENV_KEY: index}
1002
- if node.parallel:
1003
- async with sem:
1004
- return await self._exec(node.body, child_env, driver_sid)
1005
- return await self._exec(node.body, child_env, driver_sid)
1037
+ outer = _ITER_SCOPE.get()
1038
+ scope: ChainMap[str, AgentResult] = ChainMap() if outer is None else outer.new_child()
1039
+ tok = _ITER_SCOPE.set(scope)
1040
+ try:
1041
+ if node.parallel:
1042
+ async with sem:
1043
+ return await self._exec(node.body, child_env, driver_sid)
1044
+ return await self._exec(node.body, child_env, driver_sid)
1045
+ finally:
1046
+ _ITER_SCOPE.reset(tok)
1047
+ # 这一路里没成功的节点(6.46.1):全局结果表里体内 id 只留最后写的那一路,run 终态要看每一路
1048
+ for nid, r in scope.maps[0].items():
1049
+ if r.status not in ("completed", "skipped") and nid not in self._tolerated:
1050
+ self._iter_broken.append(f"{node.id or 'foreach'}[{index}].{nid}")
1006
1051
 
1007
1052
  # Per-iteration leaves are not journaled individually (see _emit_step);
1008
1053
  # the aggregate below is the resume unit. Reset even if a body raises.
@@ -667,7 +667,8 @@ def _reach_pass(
667
667
  if isinstance(node, ForeachNode):
668
668
  if node.items_from:
669
669
  _need(node.items_from, f"{path}: items_from")
670
- _reach_pass(node.body, set(available), refset, problems, f"{path}.body") # body is isolated
670
+ # body 每次迭代自成一体:体内可以引用体内先跑完的节点,体外看不到体内
671
+ _reach_pass(node.body, set(available), refset | _local_ids(node.body), problems, f"{path}.body")
671
672
  return {node.id} if node.id else set()
672
673
  if isinstance(node, LoopNode):
673
674
  # 循环体至少跑一轮:它产出的 id 在 until 判断时和循环之后都可用
@@ -679,6 +680,26 @@ def _reach_pass(
679
680
  return set()
680
681
 
681
682
 
683
+ def _local_ids(node: WorkflowNode) -> set[str]:
684
+ """一个(已解析的)子树里能被同层引用的 id:agent / foreach 聚合 / loop,不进更里层 foreach 的体。"""
685
+ out: set[str] = set()
686
+ if isinstance(node, AgentNode):
687
+ return {node.id} if node.id else out
688
+ if isinstance(node, ForeachNode):
689
+ return {node.id} if node.id else out
690
+ if isinstance(node, LoopNode):
691
+ out.add(node.id)
692
+ return out | _local_ids(node.body)
693
+ for child in (getattr(node, "steps", None) or getattr(node, "branches", None) or ()):
694
+ out |= _local_ids(child)
695
+ if isinstance(node, BranchNode):
696
+ for c in node.cases.values():
697
+ out |= _local_ids(c)
698
+ if node.default is not None:
699
+ out |= _local_ids(node.default)
700
+ return out
701
+
702
+
682
703
  def _parse_node(
683
704
  data: Any, path: str, problems: list[str], ids: set[str], body_ids: set[str]
684
705
  ) -> WorkflowNode | None:
@@ -1092,7 +1113,11 @@ def _parse_foreach(
1092
1113
  if "body" not in data:
1093
1114
  problems.append(f"{path}: foreach requires 'body'")
1094
1115
  else:
1095
- body = _parse_node(data["body"], f"{path}.body", problems, ids, body_ids)
1116
+ # 体内的节点可以引用同一个体里的节点(6.46.1:每次迭代有自己的结果表,引擎按迭代取值,不会串);
1117
+ # 体外仍然只能引用 foreach 的聚合 id。更里一层 foreach 的体照旧不对这一层开放。
1118
+ local: set[str] = set()
1119
+ _collect_referenceable_ids(data["body"], local, in_body=False)
1120
+ body = _parse_node(data["body"], f"{path}.body", problems, ids | local, body_ids - local)
1096
1121
  if body is None:
1097
1122
  return None
1098
1123
  return ForeachNode(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 6.46.0
3
+ Version: 6.46.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