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