power-loop 6.44.0__tar.gz → 6.45.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 (113) hide show
  1. {power_loop-6.44.0 → power_loop-6.45.0}/PKG-INFO +1 -1
  2. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/__init__.py +1 -1
  3. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/spec.py +29 -4
  4. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/tools/default_manifest.py +8 -3
  5. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/tools/default_tools.py +66 -4
  6. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/engine.py +40 -13
  7. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/result.py +4 -0
  8. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/spec.py +1 -1
  9. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop.egg-info/PKG-INFO +1 -1
  10. {power_loop-6.44.0 → power_loop-6.45.0}/LICENSE +0 -0
  11. {power_loop-6.44.0 → power_loop-6.45.0}/README.md +0 -0
  12. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/__init__.py +0 -0
  13. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/llm_client/__init__.py +0 -0
  14. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
  15. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/llm_client/capabilities.py +0 -0
  16. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/llm_client/interface.py +0 -0
  17. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/llm_client/llm_factory.py +0 -0
  18. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
  19. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
  20. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/llm_client/multimodal.py +0 -0
  21. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/_vendor/llm_client/proxy_fallback.py +0 -0
  22. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/agent/__init__.py +0 -0
  23. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/agent/follow_up.py +0 -0
  24. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/agent/sink.py +0 -0
  25. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/agent/stateful_loop.py +0 -0
  26. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/agent/system_prompt.py +0 -0
  27. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/agent/types.py +0 -0
  28. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/__init__.py +0 -0
  29. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/errors.py +0 -0
  30. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/event_payloads.py +0 -0
  31. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/events.py +0 -0
  32. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/handlers.py +0 -0
  33. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/hook_contexts.py +0 -0
  34. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/hooks.py +0 -0
  35. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/messages.py +0 -0
  36. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/protocols.py +0 -0
  37. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contracts/tools.py +0 -0
  38. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contrib/__init__.py +0 -0
  39. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contrib/_redact.py +0 -0
  40. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contrib/jsonl_sink.py +0 -0
  41. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contrib/logging_sink.py +0 -0
  42. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contrib/mcp.py +0 -0
  43. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contrib/metrics_sink.py +0 -0
  44. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/contrib/otel_sink.py +0 -0
  45. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/core/agent_context.py +0 -0
  46. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/core/events.py +0 -0
  47. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/core/hooks.py +0 -0
  48. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/core/phase.py +0 -0
  49. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/core/pipeline.py +0 -0
  50. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/core/runner.py +0 -0
  51. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/core/state.py +0 -0
  52. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/py.typed +0 -0
  53. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/blackboard.py +0 -0
  54. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/budget.py +0 -0
  55. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/cancellation.py +0 -0
  56. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/capability_probe.py +0 -0
  57. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/compact.py +0 -0
  58. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/env.py +0 -0
  59. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/exec_backend.py +0 -0
  60. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/fold.py +0 -0
  61. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/fold_adapter.py +0 -0
  62. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/history_projector.py +0 -0
  63. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/history_sanitize.py +0 -0
  64. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/human_input.py +0 -0
  65. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/image_recall.py +0 -0
  66. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/memory.py +0 -0
  67. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/notes.py +0 -0
  68. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/provider.py +0 -0
  69. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/representation.py +0 -0
  70. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/retry.py +0 -0
  71. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/runtime_state.py +0 -0
  72. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/session_store.py +0 -0
  73. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/skills.py +0 -0
  74. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/steer.py +0 -0
  75. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/stop_policy.py +0 -0
  76. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/__init__.py +0 -0
  77. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/backends/__init__.py +0 -0
  78. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/backends/mysql.py +0 -0
  79. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/backends/postgres.py +0 -0
  80. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/backends/sqlite.py +0 -0
  81. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/capabilities.py +0 -0
  82. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/db.py +0 -0
  83. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/dialect.py +0 -0
  84. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/factory.py +0 -0
  85. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/schema.py +0 -0
  86. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/store.py +0 -0
  87. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/store/types.py +0 -0
  88. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/structured.py +0 -0
  89. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/stub_provider.py +0 -0
  90. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/timers.py +0 -0
  91. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/todos.py +0 -0
  92. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/runtime/usage_estimate.py +0 -0
  93. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/tools/__init__.py +0 -0
  94. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/tools/blackboard.py +0 -0
  95. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/tools/command_policy.py +0 -0
  96. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/tools/command_rules.py +0 -0
  97. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/tools/registry.py +0 -0
  98. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/tools/spawn_agent.py +0 -0
  99. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/__init__.py +0 -0
  100. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/api.py +0 -0
  101. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/introspect.py +0 -0
  102. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/journal.py +0 -0
  103. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/resume.py +0 -0
  104. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/runner.py +0 -0
  105. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/subprocess_executor.py +0 -0
  106. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/tool.py +0 -0
  107. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop/workflow/worker.py +0 -0
  108. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop.egg-info/SOURCES.txt +0 -0
  109. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop.egg-info/dependency_links.txt +0 -0
  110. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop.egg-info/requires.txt +0 -0
  111. {power_loop-6.44.0 → power_loop-6.45.0}/power_loop.egg-info/top_level.txt +0 -0
  112. {power_loop-6.44.0 → power_loop-6.45.0}/pyproject.toml +0 -0
  113. {power_loop-6.44.0 → power_loop-6.45.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 6.44.0
3
+ Version: 6.45.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__ = "6.44.0"
18
+ __version__ = "6.45.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).
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
  import contextlib
22
22
  import json
23
23
  import re
24
+ from contextvars import ContextVar
24
25
  from dataclasses import dataclass, field, fields, replace
25
26
  from typing import Any
26
27
 
@@ -231,6 +232,18 @@ def _publish_subagent_event(
231
232
  pass
232
233
 
233
234
 
235
+ #: 用量收集袋(6.45,DeepTalk design/170 §5):子 agent 跑在父 agent 的工具调用里,它的用量原来只算它自己的——workflow 叶子派的
236
+ #: 子 agent 花的 token 不进叶子的用量、不扣共享预算。每次 run_agent_spec 给子 agent 开一个新袋子;它下面再派的子 agent 跑完把
237
+ #: 「自己 + 袋子里的」放进这个袋子;它自己跑完返回的 usage 也是「自己 + 袋子」,再放进上一层的袋子。
238
+ _USAGE_SINK: ContextVar[dict[str, int] | None] = ContextVar("power_loop_usage_sink", default=None)
239
+
240
+
241
+ def _add_usage(into: dict[str, int], usage: dict[str, Any] | None) -> None:
242
+ for k, v in (usage or {}).items():
243
+ if isinstance(v, (int, float)) and not isinstance(v, bool):
244
+ into[k] = int(into.get(k, 0)) + int(v)
245
+
246
+
234
247
  async def run_agent_spec(
235
248
  spec: AgentSpec | dict[str, Any] | str,
236
249
  user_input: str,
@@ -408,9 +421,20 @@ async def run_agent_spec(
408
421
  with contextlib.ExitStack() as _guard_stack:
409
422
  for _gname, _guard in getattr(parent_loop, "_child_run_guards", ()) or ():
410
423
  _guard_stack.enter_context(_guard())
411
- result = await child_loop.send(
412
- user_input, session_id=child_sid, stop_event=stop_event
413
- )
424
+ _outer_sink = _USAGE_SINK.get()
425
+ _my_sink: dict[str, int] = {}
426
+ _sink_tok = _USAGE_SINK.set(_my_sink)
427
+ try:
428
+ result = await child_loop.send(
429
+ user_input, session_id=child_sid, stop_event=stop_event
430
+ )
431
+ finally:
432
+ _USAGE_SINK.reset(_sink_tok)
433
+ _total_usage: dict[str, int] = {}
434
+ _add_usage(_total_usage, dict(result.usage or {}))
435
+ _add_usage(_total_usage, _my_sink)
436
+ if _outer_sink is not None:
437
+ _add_usage(_outer_sink, _total_usage)
414
438
  except BaseException:
415
439
  # A raised child run (cancellation, provider error, bug) would otherwise leak the EPHEMERAL
416
440
  # child session — the cleanup below is only reached on a normal return (subagent-coordination-2).
@@ -474,5 +498,6 @@ async def run_agent_spec(
474
498
  "final_text": result.final_text,
475
499
  "rounds": result.rounds,
476
500
  "depth": depth,
477
- "usage": dict(result.usage or {}),
501
+ # 自己 + 它下面所有子 agent 的用量(_USAGE_SINK)
502
+ "usage": _total_usage,
478
503
  }
@@ -352,7 +352,7 @@ DEFAULT_TOOL_DEFINITIONS: list[ToolDefinition] = [
352
352
  ToolDefinition(
353
353
  name="background_run",
354
354
  description=(
355
- "Manage private background tasks with one action: run, tool, or check. action=run "
355
+ "Manage private background tasks with one action: run, tool, check, list or wait. action=run "
356
356
  "starts a shell command in a private non-interactive background worker and returns its "
357
357
  "task_id immediately. action=tool runs one async-capable TOOL in the background "
358
358
  "(tool=<name>, args={…}) and returns a task_id at once — use it for long, "
@@ -362,7 +362,10 @@ DEFAULT_TOOL_DEFINITIONS: list[ToolDefinition] = [
362
362
  "While a task runs, do other work, or pass_turn if there is nothing else to do. "
363
363
  "action=check is a fallback: call it only when a wake message explicitly asks you to, "
364
364
  "or when you suspect a task is stuck — checking early just returns \"running\". It reports "
365
- "status/output by task_id; action=list lists all your tasks."
365
+ "status/output by task_id; action=list lists all your tasks. action=wait blocks until the "
366
+ "given task_ids (or all your running tasks) finish and returns their results — use it when "
367
+ "you cannot be woken later (e.g. inside a workflow node) or when your next step needs those "
368
+ "results; a new message cuts the wait short."
366
369
  ),
367
370
  input_schema={
368
371
  "type": "object",
@@ -372,13 +375,15 @@ DEFAULT_TOOL_DEFINITIONS: list[ToolDefinition] = [
372
375
  # list = check 不带 task_id 的显式别名(6.17.0)。原来「列出全部」这件事
373
376
  # 藏在「check 不带参数」这个隐式约定里,模型要先知道才用得上——同一件事
374
377
  # 该只有一个名字(design/96 §6.4)。
375
- "enum": ["run", "tool", "check", "list"],
378
+ "enum": ["run", "tool", "check", "list", "wait"],
376
379
  "description": "Operation to perform.",
377
380
  },
378
381
  "command": {"type": "string", "description": "Required for action=run: shell command to execute."},
379
382
  "tool": {"type": "string", "description": "Required for action=tool: name of an async-capable tool (marked in its description)."},
380
383
  "args": {"type": "object", "description": "action=tool: the tool's arguments, exactly as you would pass them when calling it directly."},
381
384
  "task_id": {"type": "string", "description": "For action=check: task to inspect; omit to list all tasks."},
385
+ "task_ids": {"type": "array", "items": {"type": "string"}, "description": "For action=wait: tasks to wait for; omit to wait for all your running tasks."},
386
+ "timeout_s": {"type": "number", "description": "For action=wait: give up waiting after this many seconds (default 600, max 3600); tasks keep running."},
382
387
  },
383
388
  "required": ["action"],
384
389
  },
@@ -1571,8 +1571,11 @@ class BackgroundManager:
1571
1571
  _a = async_capable_actions(_d)
1572
1572
  _others.append(f"{_d.name}({'/'.join(_a)})" if _a else _d.name)
1573
1573
  raise ValueError(
1574
- f"{name} 不能后台跑:它有副作用(写文件 / 发布 / 对外投递这类),"
1575
- "重跑或并发都不安全。**这件事直接调用 " + name + " 就行**,不用包一层。"
1574
+ # 只说事实:工具定义没声明 async_capable。没声明的一律按可能有副作用处理,但别断言它「有副作用」——
1575
+ # 新工具的作者只是漏标时,那句话会被当成判断(10-16 建造师 world_sketch 事故)。
1576
+ f"{name} 不能后台跑:这个工具没有声明可以后台跑(定义里没标 async_capable),"
1577
+ "没声明的一律按可能有副作用(写文件 / 发布 / 对外投递这类)处理,重跑或并发不一定安全。"
1578
+ "**这件事直接调用 " + name + " 就行**,不用包一层。"
1576
1579
  + ("\n可以后台跑的是:" + "、".join(_others) if _others else "")
1577
1580
  )
1578
1581
  store, sid = _current_store_and_session()
@@ -1759,6 +1762,61 @@ class BackgroundManager:
1759
1762
  await asyncio.wait({worker}, timeout=5)
1760
1763
  return "forced"
1761
1764
 
1765
+ async def _status_of(self, store: Any, sid: str | None, task_id: str) -> tuple[str, str]:
1766
+ """(status, 输出尾巴):优先读 store 里这个会话的行,没有就读进程内记录。"""
1767
+ if store is not None and sid is not None:
1768
+ row = await store.get_background_task(sid, task_id)
1769
+ if row is not None:
1770
+ return str(row.status), str(row.output_tail or "")
1771
+ with self._lock:
1772
+ task = self.tasks.get(task_id)
1773
+ if task is None:
1774
+ return "unknown", ""
1775
+ return str(task.get("status") or "running"), str(task.get("result") or "")
1776
+
1777
+ async def wait(self, task_ids: list[str] | None = None, timeout_s: float = 600.0) -> str:
1778
+ """等后台任务落定(6.45,DeepTalk design/170 §4):workflow 的叶子收不到后台任务的完成唤醒——它要在自己这一轮里
1779
+ 等结果。不给 ``task_ids`` 就等本会话所有还在跑的。等的时候一条插话进来就提前返回(下一轮先处理插话)。
1780
+ 返回逐件状态和输出尾巴;超时只是不再等,任务照常在跑。"""
1781
+ store, sid = _current_store_and_session()
1782
+ if store is not None and sid is not None:
1783
+ await self.flush_orphaned(store, sid)
1784
+ ids = [str(x) for x in (task_ids or []) if str(x).strip()]
1785
+ if not ids:
1786
+ if store is not None and sid is not None:
1787
+ ids = [r.task_id for r in await store.list_background_tasks(sid) if str(r.status) == "running"]
1788
+ else:
1789
+ with self._lock:
1790
+ ids = [tid for tid, tk in self.tasks.items() if tk.get("status") == "running"]
1791
+ if not ids:
1792
+ return "没有在跑的后台任务。"
1793
+ timeout_s = max(1.0, min(float(timeout_s or 600.0), 3600.0))
1794
+
1795
+ async def _poll() -> None:
1796
+ loop = asyncio.get_running_loop()
1797
+ end = loop.time() + timeout_s
1798
+ while loop.time() < end:
1799
+ states = [(await self._status_of(store, sid, tid))[0] for tid in ids]
1800
+ if all(s not in ("running", "unknown") for s in states):
1801
+ return
1802
+ futs = {self._workers[tid] for tid in ids if tid in self._workers and not self._workers[tid].done()}
1803
+ await asyncio.wait(futs or {asyncio.ensure_future(asyncio.sleep(1.0))}, timeout=1.0,
1804
+ return_when=asyncio.FIRST_COMPLETED)
1805
+
1806
+ from power_loop.runtime.steer import wait_or_steer
1807
+
1808
+ _r, steered = await wait_or_steer(_poll(), session_id=sid)
1809
+ lines = []
1810
+ pending = 0
1811
+ for tid in ids:
1812
+ status, tail = await self._status_of(store, sid, tid)
1813
+ pending += status == "running"
1814
+ lines.append(f"{tid}: [{status}] {tail[-1500:] if tail else ''}".rstrip())
1815
+ head = ("等的时候来了新消息,先停下来等(任务照常在跑)。" if steered
1816
+ else f"还有 {pending} 件没做完(等了 {int(timeout_s)} 秒,任务照常在跑)。" if pending
1817
+ else "都做完了。")
1818
+ return head + "\n" + "\n".join(lines)
1819
+
1762
1820
  async def check(self, task_id: str | None = None) -> str:
1763
1821
  store, sid = _current_store_and_session()
1764
1822
  if store is not None and sid is not None:
@@ -1814,6 +1872,7 @@ def register_tool_task_callback(cb: Any) -> None:
1814
1872
  async def run_background(
1815
1873
  action: str, command: str | None = None, task_id: str | None = None,
1816
1874
  tool: str | None = None, args: dict[str, Any] | None = None,
1875
+ task_ids: list[str] | None = None, timeout_s: float | None = None,
1817
1876
  ) -> str:
1818
1877
  """Execute one action of the merged ``background_run`` tool."""
1819
1878
  operation = str(action or "").strip().lower()
@@ -1829,7 +1888,10 @@ async def run_background(
1829
1888
  return await BG.check(None)
1830
1889
  if operation == "tool":
1831
1890
  return await BG.run_tool(tool, args)
1832
- raise ValueError("background_run action must be one of: run, check, list, tool")
1891
+ if operation == "wait":
1892
+ ids = list(task_ids or []) or ([task_id] if task_id else [])
1893
+ return await BG.wait(ids, timeout_s if timeout_s is not None else 600.0)
1894
+ raise ValueError("background_run action must be one of: run, check, list, tool, wait")
1833
1895
 
1834
1896
 
1835
1897
  async def run_todo(items: list[dict[str, Any]]) -> str:
@@ -2338,7 +2400,7 @@ async def _h_recall_send(**kw: Any) -> Any:
2338
2400
 
2339
2401
  async def _h_background_run(**kw: Any) -> Any:
2340
2402
  return await run_background(kw["action"], kw.get("command"), kw.get("task_id"),
2341
- kw.get("tool"), kw.get("args"))
2403
+ kw.get("tool"), kw.get("args"), kw.get("task_ids"), kw.get("timeout_s"))
2342
2404
 
2343
2405
 
2344
2406
  DEFAULT_TOOL_HANDLERS: dict[str, Any] = {
@@ -28,6 +28,7 @@ driver session id **inside each leaf coroutine**, so concurrent fan-out tasks
28
28
  from __future__ import annotations
29
29
 
30
30
  import asyncio
31
+ import json
31
32
  import logging
32
33
  import re
33
34
  from collections.abc import Awaitable, Callable
@@ -62,7 +63,9 @@ logger = logging.getLogger(__name__)
62
63
  #: programmatically-built spec that bypasses the static validation caps. Fail-closed when exceeded.
63
64
  MAX_TOTAL_LEAVES = 10_000
64
65
 
65
- _VAR_RE = re.compile(r"\{\{\s*([a-zA-Z_]\w*)\s*\}\}")
66
+ #: ``{{var}}``(foreach 变量等)和 ``{{node.key}}``(上游节点结构化输出里的一个键,6.45:之前不认带点的名字,
67
+ #: YAML 的 ``${{ needs.x.outputs.k }}`` 编成 ``{{x.k}}`` 后原样送给了模型)。节点 id 可以带短横线。
68
+ _VAR_RE = re.compile(r"\{\{\s*([a-zA-Z_][\w-]*(?:\.[a-zA-Z_]\w*)?)\s*\}\}")
66
69
 
67
70
  # Set while a workflow run is in flight. Propagates into child sub-agent tasks
68
71
  # (asyncio copies the context), so a leaf that calls the create_workflow tool can
@@ -150,19 +153,28 @@ class InProcessExecutor:
150
153
  ContinuationPolicy)钳制。"""
151
154
  import contextlib as _ctxlib
152
155
 
153
- with _ctxlib.ExitStack() as guards:
154
- for _name, guard in getattr(parent_loop, "_child_run_guards", ()) or ():
155
- guards.enter_context(guard())
156
- res = await parent_loop.follow_up(
157
- user_input, session_id=session_id,
158
- stop_event=stop_event, max_rounds=extra_rounds,
159
- )
156
+ from power_loop.runtime.spec import _USAGE_SINK, _add_usage
157
+
158
+ sink: dict[str, int] = {} # 续跑这几轮里派的子 agent 的用量也算进来(design/170 §5)
159
+ sink_tok = _USAGE_SINK.set(sink)
160
+ try:
161
+ with _ctxlib.ExitStack() as guards:
162
+ for _name, guard in getattr(parent_loop, "_child_run_guards", ()) or ():
163
+ guards.enter_context(guard())
164
+ res = await parent_loop.follow_up(
165
+ user_input, session_id=session_id,
166
+ stop_event=stop_event, max_rounds=extra_rounds,
167
+ )
168
+ finally:
169
+ _USAGE_SINK.reset(sink_tok)
160
170
  status = str(getattr(res, "status", "failed") or "failed")
171
+ usage = await _usage_delta(parent_loop, session_id, getattr(res, "usage", None))
172
+ _add_usage(usage, sink)
161
173
  return {
162
174
  "status": status,
163
175
  "final_text": str(getattr(res, "final_text", "") or ""),
164
176
  "rounds": int(getattr(res, "rounds", 0) or 0),
165
- "usage": await _usage_delta(parent_loop, session_id, getattr(res, "usage", None)),
177
+ "usage": usage,
166
178
  "session_id": session_id,
167
179
  }
168
180
 
@@ -369,6 +381,7 @@ class WorkflowEngine:
369
381
  final=self._last,
370
382
  usage=self._total_usage(),
371
383
  errors=list(self._errors),
384
+ tolerated=sorted(self._tolerated),
372
385
  )
373
386
 
374
387
  # ── node dispatch ──────────────────────────────────────────────────────
@@ -421,7 +434,7 @@ class WorkflowEngine:
421
434
  return res
422
435
  self._leaves_run += 1
423
436
 
424
- user_input = _render(node.input, env)
437
+ user_input = _render(node.input, env, self._resolve_ref_soft)
425
438
  if node.inputs_from:
426
439
  extras = []
427
440
  for ref in node.inputs_from:
@@ -869,6 +882,16 @@ class WorkflowEngine:
869
882
  )
870
883
  return res.payload[key]
871
884
 
885
+ def _resolve_ref_soft(self, ref: str) -> str:
886
+ """渲染节点输入里的 ``{{node.key}}``:取不到时给一句明确说明,不让整个 run 崩(下游自己判断怎么办)。"""
887
+ try:
888
+ value = self._resolve_ref(ref)
889
+ except WorkflowRunError as exc:
890
+ return f"[取不到 {ref}:{exc}]"
891
+ if isinstance(value, (dict, list)):
892
+ return json.dumps(value, ensure_ascii=False)
893
+ return str(value)
894
+
872
895
  def _total_usage(self) -> dict[str, int]:
873
896
  # Accumulated once per real leaf / replayed result — not summed from
874
897
  # _results (where foreach bodies share one id and would mis-count).
@@ -883,11 +906,15 @@ def _sum_usage(results: list[AgentResult]) -> dict[str, int]:
883
906
  return total
884
907
 
885
908
 
886
- def _render(template: str, env: dict[str, Any]) -> str:
887
- """Substitute ``{{var}}`` from ``env``; leave unknown placeholders untouched."""
909
+ def _render(template: str, env: dict[str, Any], resolve_ref: Callable[[str], str] | None = None) -> str:
910
+ """Substitute ``{{var}}`` from ``env`` and ``{{node.key}}`` via ``resolve_ref``; leave unknown placeholders untouched."""
888
911
 
889
912
  def repl(m: re.Match[str]) -> str:
890
913
  name = m.group(1)
891
- return str(env[name]) if name in env else m.group(0)
914
+ if name in env:
915
+ return str(env[name])
916
+ if "." in name and resolve_ref is not None:
917
+ return resolve_ref(name)
918
+ return m.group(0)
892
919
 
893
920
  return _VAR_RE.sub(repl, template)
@@ -69,6 +69,9 @@ class WorkflowResult:
69
69
  final: AgentResult | None = None
70
70
  usage: dict[str, int] = field(default_factory=dict)
71
71
  errors: list[str] = field(default_factory=list)
72
+ #: 失败但明确放行(continue_on_error)的节点 id(6.45):宿主自己再判一遍 run 终态时要跳过它们,
73
+ #: 否则 continue_on_error 在宿主侧等于没开(DeepTalk design/170 §6)。
74
+ tolerated: list[str] = field(default_factory=list)
72
75
 
73
76
  @property
74
77
  def ok(self) -> bool:
@@ -98,6 +101,7 @@ class WorkflowResult:
98
101
  "final": self.final.to_dict() if self.final else None,
99
102
  "usage": self.usage,
100
103
  "errors": self.errors,
104
+ "tolerated": self.tolerated,
101
105
  }
102
106
 
103
107
 
@@ -45,7 +45,7 @@ from typing import Any
45
45
  from power_loop.contracts.errors import SpecValidationError
46
46
  from power_loop.runtime.spec import AgentSpec, AgentSpecError
47
47
 
48
- # Template variable identifier grammar — must match engine._VAR_RE's capture (`[a-zA-Z_]\w*`), so a
48
+ # Template variable identifier grammar — a subset of engine._VAR_RE's capture (which also allows {{node.key}}), so a
49
49
  # foreach `as` name validated here is guaranteed substitutable as {{name}} in the body.
50
50
  _IDENTIFIER_RE = re.compile(r"^[A-Za-z_]\w*$")
51
51
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 6.44.0
3
+ Version: 6.45.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
File without changes
File without changes
File without changes
File without changes