power-loop 6.46.4__tar.gz → 6.48.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.46.4 → power_loop-6.48.0}/PKG-INFO +1 -1
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/__init__.py +1 -1
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/agent/stateful_loop.py +7 -3
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/agent/types.py +3 -7
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/core/pipeline.py +62 -192
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/representation.py +7 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/tools/default_manifest.py +3 -1
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/tools/default_tools.py +46 -20
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/engine.py +147 -3
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/resume.py +98 -20
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/runner.py +15 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop.egg-info/PKG-INFO +1 -1
- {power_loop-6.46.4 → power_loop-6.48.0}/LICENSE +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/README.md +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/__init__.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/llm_client/__init__.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/llm_client/capabilities.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/llm_client/interface.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/llm_client/llm_factory.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/llm_client/multimodal.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/_vendor/llm_client/proxy_fallback.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/agent/__init__.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/agent/follow_up.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/agent/sink.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/agent/system_prompt.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/__init__.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/errors.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/event_payloads.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/events.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/handlers.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/hook_contexts.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/hooks.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/messages.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/protocols.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contracts/tools.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contrib/__init__.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contrib/_redact.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contrib/jsonl_sink.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contrib/logging_sink.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contrib/mcp.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contrib/metrics_sink.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/contrib/otel_sink.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/core/agent_context.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/core/events.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/core/hooks.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/core/phase.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/core/runner.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/core/state.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/py.typed +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/blackboard.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/budget.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/cancellation.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/capability_probe.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/compact.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/env.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/exec_backend.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/fold.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/fold_adapter.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/history_projector.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/history_sanitize.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/human_input.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/image_recall.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/memory.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/notes.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/provider.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/retry.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/runtime_state.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/session_store.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/skills.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/spec.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/steer.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/stop_policy.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/__init__.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/backends/__init__.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/backends/mysql.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/backends/postgres.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/backends/sqlite.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/capabilities.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/db.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/dialect.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/factory.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/schema.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/store.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/store/types.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/structured.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/stub_provider.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/timers.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/todos.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/runtime/usage_estimate.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/tools/__init__.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/tools/blackboard.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/tools/command_policy.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/tools/command_rules.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/tools/registry.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/tools/spawn_agent.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/__init__.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/api.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/introspect.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/journal.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/result.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/spec.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/subprocess_executor.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/tool.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop/workflow/worker.py +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop.egg-info/SOURCES.txt +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop.egg-info/dependency_links.txt +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop.egg-info/requires.txt +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/power_loop.egg-info/top_level.txt +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/pyproject.toml +0 -0
- {power_loop-6.46.4 → power_loop-6.48.0}/setup.cfg +0 -0
|
@@ -15,7 +15,7 @@ Stability tiers
|
|
|
15
15
|
无版本承诺,可随时变更或删除。
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
__version__ = "6.
|
|
18
|
+
__version__ = "6.48.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).
|
|
@@ -785,6 +785,7 @@ class StatefulAgentLoop:
|
|
|
785
785
|
tools: Sequence[str] | ToolRegistry | None = None,
|
|
786
786
|
system_prompt: str | None = None,
|
|
787
787
|
max_rounds: int | None = None,
|
|
788
|
+
enqueue_only: bool = False,
|
|
788
789
|
) -> StatefulResult | FollowUpQueued:
|
|
789
790
|
"""Put ``items`` in front of the session's model — now, or as soon as it can take them.
|
|
790
791
|
|
|
@@ -798,6 +799,8 @@ class StatefulAgentLoop:
|
|
|
798
799
|
them. Returns :class:`FollowUpQueued`.
|
|
799
800
|
* idle → a fresh send runs with the waiting items as its input (everything pending —
|
|
800
801
|
including items a cancelled run left behind — oldest first). Returns its result.
|
|
802
|
+
* ``enqueue_only=True`` → persist and return without starting an idle send;
|
|
803
|
+
the orchestration owner must drain it, including the terminal window.
|
|
801
804
|
* nothing new (all duplicates) → :class:`FollowUpQueued` with ``accepted=0``.
|
|
802
805
|
|
|
803
806
|
``stop_event`` / ``tools`` / ``system_prompt`` / ``max_rounds`` apply only to a run this
|
|
@@ -824,7 +827,7 @@ class StatefulAgentLoop:
|
|
|
824
827
|
duplicates=len(rows) - len(accepted), accepted_ids=tuple(accepted),
|
|
825
828
|
)
|
|
826
829
|
|
|
827
|
-
if not accepted or self._lock_for(sid).locked():
|
|
830
|
+
if enqueue_only or not accepted or self._lock_for(sid).locked():
|
|
828
831
|
return await _queued()
|
|
829
832
|
if self.config.distributed_sessions:
|
|
830
833
|
# Idle HERE, but another process may be driving this session: its run claims the
|
|
@@ -847,6 +850,7 @@ class StatefulAgentLoop:
|
|
|
847
850
|
tools: Sequence[str] | ToolRegistry | None = None,
|
|
848
851
|
system_prompt: str | None = None,
|
|
849
852
|
max_rounds: int | None = None,
|
|
853
|
+
enqueue_only: bool = False,
|
|
850
854
|
kind: str = INBOX_KIND_USER,
|
|
851
855
|
mode: str = "queue",
|
|
852
856
|
item_id: str | None = None,
|
|
@@ -865,7 +869,7 @@ class StatefulAgentLoop:
|
|
|
865
869
|
return await self.deliver(
|
|
866
870
|
InboxItem(content=user_input, kind=kind, mode=mode, item_id=item_id),
|
|
867
871
|
session_id, stop_event=stop_event, tools=tools, system_prompt=system_prompt,
|
|
868
|
-
max_rounds=max_rounds,
|
|
872
|
+
max_rounds=max_rounds, enqueue_only=enqueue_only,
|
|
869
873
|
)
|
|
870
874
|
|
|
871
875
|
def pending_follow_up_count(self, session_id: str) -> int:
|
|
@@ -1268,7 +1272,7 @@ class StatefulAgentLoop:
|
|
|
1268
1272
|
jobs.append({"id": "send", "kind": "foreground", "title": "当前这一轮",
|
|
1269
1273
|
"status": "running", "started_at": None, "origin": ""})
|
|
1270
1274
|
for t in await store.list_background_tasks(session_id):
|
|
1271
|
-
if t.status
|
|
1275
|
+
if t.status not in ("queued", "running"):
|
|
1272
1276
|
continue
|
|
1273
1277
|
jobs.append({"id": f"bg:{t.task_id}", "kind": "background",
|
|
1274
1278
|
"title": str(t.command or "")[:80], "status": t.status,
|
|
@@ -106,14 +106,10 @@ class AgentLoopConfig:
|
|
|
106
106
|
#: 真实大小)≥ 它 → 以 ``context_checkpoint`` 优雅收尾本 send(COMPLETE_DECIDE 先给宿主收尾窗口)
|
|
107
107
|
#: → 正常投影 → 宿主的续接机制在新 send 里接着干。与 ``max_tokens_per_run``(累计费用上限)正交。
|
|
108
108
|
context_checkpoint_tokens: int | None = None
|
|
109
|
-
#:
|
|
110
|
-
#:
|
|
111
|
-
#: 新增的行),把当前 send 里热尾之外的冷工具结果**在内存里替换成它的投影行**(同一套
|
|
112
|
-
#: ToolDefinition.project 蒸馏 + ``recall_send`` 坐标),不落盘、不改 pl_messages。None → 关。
|
|
113
|
-
#: 6.27.0 之前量的是整个上下文(系统提示词 + 历史 + 本 send),一开局就可能超阈值、每轮都烧。
|
|
109
|
+
#: Total prompt threshold for in-send semantic distillation. When enabled,
|
|
110
|
+
#: context_checkpoint_tokens is ignored: fuse and cut are exclusive policies.
|
|
114
111
|
insend_distill_tokens: int | None = None
|
|
115
|
-
#:
|
|
116
|
-
#: 且永远不动最近 ``hot_tail`` 条。触发依据优先用上一轮供应商返回的真实 prompt_tokens。
|
|
112
|
+
#: Deprecated compatibility field; does not limit the number of distilled calls.
|
|
117
113
|
insend_distill_batch: int = 10
|
|
118
114
|
insend_distill_hot_tail: int = 8
|
|
119
115
|
#: 6.27.0 这些工具的**调用参数永不瘦身**(结果照旧)。给「模型自己说的话」用:send_message /
|
|
@@ -882,76 +882,6 @@ class AgentPipeline:
|
|
|
882
882
|
|
|
883
883
|
_DISTILL_MARK = "[distilled #"
|
|
884
884
|
|
|
885
|
-
#: 参数被瘦身后留在原位的标记(值本身仍是合法 JSON 字符串,工具早已执行完,
|
|
886
|
-
#: 这里只影响模型回看时读到什么)。
|
|
887
|
-
_ARGS_MARK = "⟨已移出上下文"
|
|
888
|
-
#: 单个参数字段超过它才瘦身:短字段(路径、开关、行号)留着才看得出这次调用干了什么。
|
|
889
|
-
_ARGS_KEEP_CHARS = 200
|
|
890
|
-
|
|
891
|
-
def _has_heavy_args(self, idx: int) -> bool:
|
|
892
|
-
"""这一行是 assistant,且它的某个 tool_calls 参数里有还没瘦过的大字段。"""
|
|
893
|
-
m = self.history[idx]
|
|
894
|
-
if m.get("role") != "assistant":
|
|
895
|
-
return False
|
|
896
|
-
keep = set(getattr(self.config, "insend_distill_keep_tools", ()) or ())
|
|
897
|
-
for tc in m.get("tool_calls") or []:
|
|
898
|
-
fn = (tc or {}).get("function") or {}
|
|
899
|
-
if str(fn.get("name") or "") in keep:
|
|
900
|
-
continue
|
|
901
|
-
raw = fn.get("arguments")
|
|
902
|
-
if (
|
|
903
|
-
isinstance(raw, str)
|
|
904
|
-
and len(raw) > 300
|
|
905
|
-
and self._ARGS_MARK not in raw
|
|
906
|
-
):
|
|
907
|
-
return True
|
|
908
|
-
return False
|
|
909
|
-
|
|
910
|
-
def _distill_tool_call_args(self, idx: int, seqs: list[Any]) -> int:
|
|
911
|
-
"""把一行 assistant 的 tool_calls 参数里的大字段换成指针,**保持合法 JSON**。
|
|
912
|
-
|
|
913
|
-
为什么不套用投影那套逐工具规则:那边产出的是给人读的一行散文,而这里改的是
|
|
914
|
-
供应商要按 JSON 解析的 ``function.arguments``。所以规则通用——短字段原样留下
|
|
915
|
-
(路径、模式、行号这些才看得出这次调用干了什么),只有超过 ``_ARGS_KEEP_CHARS``
|
|
916
|
-
的长字符串换成一句带 ``recall_send`` 坐标的说明。工具早已执行完,改它不影响任何
|
|
917
|
-
副作用,只影响模型回看时读到什么。返回释放的估算 token。"""
|
|
918
|
-
m = self.history[idx]
|
|
919
|
-
seq = seqs[idx] if idx < len(seqs) else None
|
|
920
|
-
coord = (
|
|
921
|
-
f"recall_send(send_index={self.send_index}, seq={seq})"
|
|
922
|
-
if (self.send_index is not None and seq is not None)
|
|
923
|
-
else "recall_send"
|
|
924
|
-
)
|
|
925
|
-
freed = 0
|
|
926
|
-
keep = set(getattr(self.config, "insend_distill_keep_tools", ()) or ())
|
|
927
|
-
for tc in m.get("tool_calls") or []:
|
|
928
|
-
fn = (tc or {}).get("function") or {}
|
|
929
|
-
if str(fn.get("name") or "") in keep:
|
|
930
|
-
continue # 模型自己的发言(send_message 之类):瘦成占位符它会照样再发一遍
|
|
931
|
-
raw = fn.get("arguments")
|
|
932
|
-
if not isinstance(raw, str) or len(raw) <= 300 or self._ARGS_MARK in raw:
|
|
933
|
-
continue
|
|
934
|
-
try:
|
|
935
|
-
args = json.loads(raw)
|
|
936
|
-
except (TypeError, ValueError):
|
|
937
|
-
continue
|
|
938
|
-
if not isinstance(args, dict):
|
|
939
|
-
continue
|
|
940
|
-
slim = {
|
|
941
|
-
k: (
|
|
942
|
-
f"{self._ARGS_MARK} {len(v)} 字符,需要时 {coord} 回取⟩"
|
|
943
|
-
if isinstance(v, str) and len(v) > self._ARGS_KEEP_CHARS
|
|
944
|
-
else v
|
|
945
|
-
)
|
|
946
|
-
for k, v in args.items()
|
|
947
|
-
}
|
|
948
|
-
new_raw = json.dumps(slim, ensure_ascii=False)
|
|
949
|
-
if len(new_raw) >= len(raw):
|
|
950
|
-
continue # 没省下就别动(改了反而打断前缀缓存)
|
|
951
|
-
fn["arguments"] = new_raw
|
|
952
|
-
freed += (len(raw) - len(new_raw)) // 4
|
|
953
|
-
return freed
|
|
954
|
-
|
|
955
885
|
def _context_profile(self, start: int, limit: int = 4) -> str:
|
|
956
886
|
"""本 send 的上下文被谁占着——`工具名:来源=字符数`,最大的几个。
|
|
957
887
|
|
|
@@ -978,106 +908,65 @@ class AgentPipeline:
|
|
|
978
908
|
top = sorted(buckets.items(), key=lambda kv: -kv[1])[:limit]
|
|
979
909
|
return " ".join(f"{k}={v // 1000}K" for k, v in top if v >= 1000) or "(无大项)"
|
|
980
910
|
|
|
981
|
-
def _distill_oldest_tool_rows(self, batch: int, hot_tail: int) -> int:
|
|
982
|
-
"""
|
|
911
|
+
def _distill_oldest_tool_rows(self, batch: int, hot_tail: int, *, target_tokens: int | None = None) -> int:
|
|
912
|
+
"""Distill complete calls oldest first; ``batch`` is ignored for compatibility.
|
|
983
913
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
投影行 = 与 end-of-send 投影**同一套** ``ToolDefinition.project`` 蒸馏(找不到钩子就截断),
|
|
988
|
-
带 ``recall_send(send_index, seq)`` 坐标——原文永远在 pl_messages,要用就回取。不落盘、不改
|
|
989
|
-
数据库、不改行数(sink 的 index↔seq 对齐不受影响)。最近 ``hot_tail`` 条永不动。verbatim
|
|
990
|
-
模式不做(那边有就地压缩器)。返回本批释放的估算 token(≈字符/4)。"""
|
|
914
|
+
Protect invocation IDs, not result rows: parallel results may arrive out of order.
|
|
915
|
+
Only replace in-memory copies, keeping persisted audit records and seq alignment intact.
|
|
916
|
+
"""
|
|
991
917
|
rep = self.config.representation
|
|
992
|
-
|
|
918
|
+
distill = getattr(rep, "distill_tool_call", None)
|
|
919
|
+
if getattr(rep, "kind", "projection") == "verbatim" or distill is None:
|
|
993
920
|
return 0
|
|
994
|
-
project_tool = getattr(rep, "_project_tool", None)
|
|
995
921
|
seqs = getattr(self.sink, "_history_seqs", None) or []
|
|
996
|
-
# 6.27.0 只动**本 send** 追加的行:run 之前就在历史里的(上一个 send 留下的逐字行)归跨 send
|
|
997
|
-
# 投影管,保险丝碰它们等于两套机制抢同一段历史。
|
|
998
922
|
start = min(int(getattr(self, "_send_start_len", 0) or 0), len(self.history))
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
)
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
picked = [i for kind, i in picked_all if kind == "result"]
|
|
1030
|
-
picked_args = [i for kind, i in picked_all if kind == "args"]
|
|
1031
|
-
if not picked_all:
|
|
1032
|
-
return 0
|
|
1033
|
-
# tool_call_id → (name, args) 从前面的 assistant(tool_calls) 行反查。
|
|
1034
|
-
# 只解析**本批选中的**那几个 id:以前是每次触发都把全历史所有 assistant 行的所有
|
|
1035
|
-
# tool_calls 的 arguments 反序列化一遍建全表,只为查其中 batch 条(默认 10)。
|
|
1036
|
-
# 保险丝一旦启动几乎每轮都触发,于是每轮做一次 O(历史) 的 json.loads。
|
|
1037
|
-
wanted = {str(self.history[i].get("tool_call_id") or "") for i in picked}
|
|
1038
|
-
wanted.discard("")
|
|
1039
|
-
call_meta: dict[str, tuple[str, Any]] = {}
|
|
1040
|
-
for m in self.history if wanted else ():
|
|
1041
|
-
if m.get("role") != "assistant":
|
|
923
|
+
calls = []
|
|
924
|
+
results = {}
|
|
925
|
+
for i in range(start, len(self.history)):
|
|
926
|
+
row = self.history[i]
|
|
927
|
+
if row.get("role") == "assistant":
|
|
928
|
+
for j, tc in enumerate(row.get("tool_calls") or []):
|
|
929
|
+
if tc.get("id"):
|
|
930
|
+
calls.append((str(tc["id"]), i, j))
|
|
931
|
+
elif row.get("role") == "tool":
|
|
932
|
+
results[str(row.get("tool_call_id") or "")] = i
|
|
933
|
+
protected = {cid for cid, _, _ in calls[-hot_tail:]} if hot_tail > 0 else set()
|
|
934
|
+
keep = set(self.config.insend_distill_keep_tools or ())
|
|
935
|
+
freed_chars = 0
|
|
936
|
+
for cid, i, j in calls:
|
|
937
|
+
if cid in protected or cid not in results:
|
|
938
|
+
continue
|
|
939
|
+
ri = results[cid]
|
|
940
|
+
content = self.history[ri].get("content")
|
|
941
|
+
tc = self.history[i]["tool_calls"][j]
|
|
942
|
+
fn = tc.get("function") or {}
|
|
943
|
+
raw = fn.get("arguments")
|
|
944
|
+
name = str(fn.get("name") or "tool")
|
|
945
|
+
if (name in keep or not isinstance(content, str) or not isinstance(raw, str)
|
|
946
|
+
or content.startswith(self._DISTILL_MARK)):
|
|
947
|
+
continue
|
|
948
|
+
try:
|
|
949
|
+
args = json.loads(raw)
|
|
950
|
+
line = distill(name, args, content, getattr(self, "tool_registry", None))
|
|
951
|
+
except Exception: # a broken projector must not stop the run or starve later calls
|
|
952
|
+
logger.warning("in-send distillation failed for %s", name, exc_info=True)
|
|
1042
953
|
continue
|
|
1043
|
-
for tc in m.get("tool_calls") or []:
|
|
1044
|
-
cid = str((tc or {}).get("id") or "")
|
|
1045
|
-
if cid not in wanted or cid in call_meta:
|
|
1046
|
-
continue
|
|
1047
|
-
fn = (tc or {}).get("function") or {}
|
|
1048
|
-
args = fn.get("arguments")
|
|
1049
|
-
if isinstance(args, str):
|
|
1050
|
-
try:
|
|
1051
|
-
args = json.loads(args)
|
|
1052
|
-
except (TypeError, ValueError):
|
|
1053
|
-
args = {"raw": args[:200]}
|
|
1054
|
-
call_meta[cid] = (str(fn.get("name") or "tool"), args)
|
|
1055
|
-
if len(call_meta) == len(wanted):
|
|
1056
|
-
break
|
|
1057
|
-
freed = 0
|
|
1058
|
-
for i in picked:
|
|
1059
|
-
m = self.history[i]
|
|
1060
|
-
name, args = call_meta.get(str(m.get("tool_call_id") or ""), ("tool", {}))
|
|
1061
|
-
content = str(m.get("content") or "")
|
|
1062
|
-
if project_tool is not None:
|
|
1063
|
-
try:
|
|
1064
|
-
d = project_tool(name, args, content, getattr(self, "tool_registry", None))
|
|
1065
|
-
parts = [f"{k}={v}" for k, v in (d or {}).items() if k != "name" and v not in (None, "")]
|
|
1066
|
-
line = f"{name}(" + ", ".join(str(x)[:200] for x in parts) + ")"
|
|
1067
|
-
except Exception: # noqa: BLE001 — 蒸馏钩子出错就退回截断
|
|
1068
|
-
line = f"{name}: {content[:200]}…"
|
|
1069
|
-
else:
|
|
1070
|
-
line = f"{name}: {content[:200]}…"
|
|
1071
954
|
seq = seqs[i] if i < len(seqs) else None
|
|
1072
955
|
coord = (f"recall_send(send_index={self.send_index}, seq={seq})"
|
|
1073
|
-
if
|
|
1074
|
-
new_content =
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
956
|
+
if self.send_index is not None and seq is not None else "recall_send")
|
|
957
|
+
new_content = f"{self._DISTILL_MARK}{self.send_index} seq={seq}; {coord}] {line}"
|
|
958
|
+
new_raw = json.dumps({"_distilled": coord}, ensure_ascii=False)
|
|
959
|
+
saved = len(raw) + len(content) - len(new_raw) - len(new_content)
|
|
960
|
+
if saved <= 0:
|
|
961
|
+
continue
|
|
962
|
+
new_calls = list(self.history[i]["tool_calls"])
|
|
963
|
+
new_calls[j] = {**tc, "function": {**fn, "arguments": new_raw}}
|
|
964
|
+
self.history[i] = {**self.history[i], "tool_calls": new_calls}
|
|
965
|
+
self.history[ri] = {**self.history[ri], "content": new_content}
|
|
966
|
+
freed_chars += saved
|
|
967
|
+
if target_tokens is not None and freed_chars // 4 >= target_tokens:
|
|
968
|
+
break
|
|
969
|
+
return freed_chars // 4
|
|
1081
970
|
|
|
1082
971
|
def _estimate_history_tokens(self) -> int:
|
|
1083
972
|
"""Current history token estimate, maintained incrementally (SCALE-4).
|
|
@@ -1131,42 +1020,22 @@ class AgentPipeline:
|
|
|
1131
1020
|
if irr is not None and int(irr) > 0 and self._retire_stale_images(round_index, int(irr)):
|
|
1132
1021
|
self._tok_len = -1
|
|
1133
1022
|
|
|
1134
|
-
#
|
|
1135
|
-
# 触发依据 = 上一轮供应商返回的真实 prompt_tokens(就是当前上下文的真实大小;第一轮
|
|
1136
|
-
# 还没有就用估算)。每轮最多蒸馏一批(最早的 n 条、跳过已蒸馏的、不动最近 hot_tail 条);
|
|
1137
|
-
# 下一轮真实 prompt 仍超阈值就再蒸馏下一批——逐轮递进,只蒸馏到够用为止。
|
|
1023
|
+
# Fuse uses total prompt size, including the fixed prompt and history prefix.
|
|
1138
1024
|
dt = self.config.insend_distill_tokens
|
|
1139
1025
|
if dt is not None and int(dt) > 0 and round_index > 0:
|
|
1140
|
-
# 6.27.0:量的是**本 send 内的增长**,不是整个上下文。系统提示词 + 技能 + 工具目录 + 历史
|
|
1141
|
-
# 一开局就可能有几万 token,按整体判的话第一轮就超阈值、之后每轮都烧(真实事故:一个
|
|
1142
|
-
# 会话 44 次触发,模型自己的发言参数被瘦成占位符,再被照样发进聊天)。保险丝的本意是
|
|
1143
|
-
# 「这一个 send 跑得太长」才出手:第一轮的真实 prompt_tokens 是基线,之后每轮减它。
|
|
1144
1026
|
real = int((self.ctx.token_usage or {}).get("prompt_tokens", 0) or 0)
|
|
1145
|
-
if real
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
else:
|
|
1150
|
-
start = min(int(getattr(self, "_send_start_len", 0) or 0), len(self.history))
|
|
1151
|
-
growth = estimate_tokens(self.history[start:])
|
|
1152
|
-
if growth >= int(dt):
|
|
1027
|
+
if real and self._send_baseline_prompt is None:
|
|
1028
|
+
self._send_baseline_prompt = real # diagnostics only; never subtracted from trigger
|
|
1029
|
+
total = real or self._estimate_history_tokens()
|
|
1030
|
+
if total >= int(dt):
|
|
1153
1031
|
freed = self._distill_oldest_tool_rows(
|
|
1154
|
-
int(self.config.
|
|
1155
|
-
int(
|
|
1032
|
+
0, int(self.config.insend_distill_hot_tail or 0),
|
|
1033
|
+
target_tokens=max(1, total - int(dt) + 1),
|
|
1156
1034
|
)
|
|
1157
1035
|
if freed:
|
|
1158
|
-
self._tok_len = -1
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
# 而它的表现和「一切正常」一模一样——上下文继续涨,日志里一个字都没有。
|
|
1162
|
-
logger.info(
|
|
1163
|
-
"insend distill: send grew %d >= %d tokens; freed ~%d this round "
|
|
1164
|
-
"(send %s, round %d, prompt %d, rows %d) | 大头: %s%s",
|
|
1165
|
-
growth, int(dt), freed, self.send_index, round_index, real,
|
|
1166
|
-
len(self.history) - start_i, self._context_profile(start_i),
|
|
1167
|
-
"" if freed else " ← 没有可回收的了:剩下的要么在热尾、要么已蒸馏、要么是"
|
|
1168
|
-
"模型自己的正文(保险丝只动工具结果与调用参数)",
|
|
1169
|
-
)
|
|
1036
|
+
self._tok_len = -1
|
|
1037
|
+
logger.info("insend distill: total prompt %d >= %d; freed ~%d tokens",
|
|
1038
|
+
total, int(dt), freed)
|
|
1170
1039
|
if self.config.microcompact_enabled:
|
|
1171
1040
|
self.ctx.microcompact(
|
|
1172
1041
|
self.history,
|
|
@@ -1665,6 +1534,7 @@ class AgentPipeline:
|
|
|
1665
1534
|
ckpt = self.config.context_checkpoint_tokens
|
|
1666
1535
|
if (
|
|
1667
1536
|
ckpt is not None
|
|
1537
|
+
and (self.config.insend_distill_tokens or 0) <= 0
|
|
1668
1538
|
and int(ckpt) > 0
|
|
1669
1539
|
and round_idx > 0
|
|
1670
1540
|
and not self._terminal_grace_active
|
|
@@ -420,6 +420,13 @@ class ProjectedRepresentation:
|
|
|
420
420
|
source_seq_hi=max(seqs) if seqs else None,
|
|
421
421
|
)
|
|
422
422
|
|
|
423
|
+
def distill_tool_call(
|
|
424
|
+
self, name: str, args: Any, result: Any, tool_registry: ToolRegistry | None
|
|
425
|
+
) -> str:
|
|
426
|
+
"""Public single-call projection seam shared with the in-send context fuse."""
|
|
427
|
+
entry = self._project_tool(name, args, result, tool_registry)
|
|
428
|
+
return json.dumps(entry, ensure_ascii=False, default=str)
|
|
429
|
+
|
|
423
430
|
def _project_tool(
|
|
424
431
|
self, name: str | None, args: Any, result: Any, tool_registry: ToolRegistry | None
|
|
425
432
|
) -> dict[str, Any]:
|
|
@@ -357,7 +357,9 @@ DEFAULT_TOOL_DEFINITIONS: list[ToolDefinition] = [
|
|
|
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, "
|
|
359
359
|
"side-effect-free calls (image generation, web fetches) whose result you do not "
|
|
360
|
-
"need immediately.
|
|
360
|
+
"need immediately. Up to 8 submitted tools execute per session; excess calls are accepted "
|
|
361
|
+
"as queued and start automatically in submission order when a slot opens. No check is needed "
|
|
362
|
+
"to release a slot. **Results are delivered to you automatically on completion "
|
|
361
363
|
"(in the background updates block or a wake message) — do not poll for them.** "
|
|
362
364
|
"While a task runs, do other work, or pass_turn if there is nothing else to do. "
|
|
363
365
|
"action=check is a fallback: call it only when a wake message explicitly asks you to, "
|
|
@@ -1262,6 +1262,7 @@ class BackgroundManager:
|
|
|
1262
1262
|
# agent when they settle — the user said stop).
|
|
1263
1263
|
self._stop_tokens: dict[str, Any] = {}
|
|
1264
1264
|
self._stopped: set[str] = set()
|
|
1265
|
+
self._tool_slots: dict[tuple[Any, int, str | None], tuple[asyncio.Semaphore, int]] = {}
|
|
1265
1266
|
# Live daemon threads (so shutdown can drain them) and terminal write-backs that
|
|
1266
1267
|
# could not be delivered to their owning loop (so a later check / shutdown can
|
|
1267
1268
|
# still persist them instead of leaving the row stuck at 'running' forever).
|
|
@@ -1274,7 +1275,7 @@ class BackgroundManager:
|
|
|
1274
1275
|
Running tasks are never evicted. Caller must hold ``self._lock``."""
|
|
1275
1276
|
while len(self.tasks) > _MAX_BG_TASKS:
|
|
1276
1277
|
for tid, t in self.tasks.items(): # insertion order = oldest first
|
|
1277
|
-
if t.get("status")
|
|
1278
|
+
if t.get("status") not in ("queued", "running"):
|
|
1278
1279
|
del self.tasks[tid]
|
|
1279
1280
|
self._threads.pop(tid, None)
|
|
1280
1281
|
break
|
|
@@ -1527,9 +1528,9 @@ class BackgroundManager:
|
|
|
1527
1528
|
计费/活动打标上下文天然跟着走,不需要捕获重播。结果持久化进同一张
|
|
1528
1529
|
background 任务表(command 字段以 ``tool:`` 前缀区分);完成后调用宿主注册的
|
|
1529
1530
|
``on_tool_task_complete``(best-effort),由宿主决定要不要唤醒睡着的 agent。
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1531
|
+
每个 store/sid 的新调用最多 8 个同时执行,超出后按接收顺序等待。
|
|
1532
|
+
队列驻留当前进程;宿主重启时应把旧 queued/running 行标为 interrupted,
|
|
1533
|
+
不自动重放可能已经产生外部副作用的调用。adopt 接管已经执行的调用,不重新排队。
|
|
1533
1534
|
"""
|
|
1534
1535
|
from power_loop.core.agent_context import get_current_loop
|
|
1535
1536
|
|
|
@@ -1579,21 +1580,21 @@ class BackgroundManager:
|
|
|
1579
1580
|
+ ("\n可以后台跑的是:" + "、".join(_others) if _others else "")
|
|
1580
1581
|
)
|
|
1581
1582
|
store, sid = _current_store_and_session()
|
|
1582
|
-
if store is not None and sid is not None:
|
|
1583
|
-
rows = await store.list_background_tasks(sid)
|
|
1584
|
-
running = sum(1 for r in rows if r.status == "running" and r.command.startswith("tool:"))
|
|
1585
|
-
if running >= 8:
|
|
1586
|
-
raise ValueError("后台工具任务已达并发上限(8)——先 check 收结果再发新的。")
|
|
1587
1583
|
task_id = str(uuid.uuid4())[:8]
|
|
1588
1584
|
args = dict(args or {})
|
|
1589
1585
|
label = f"tool:{name} " + json.dumps(args, ensure_ascii=False)[:160]
|
|
1590
|
-
with self._lock:
|
|
1591
|
-
self.tasks[task_id] = {"command": label, "status": "running", "result": None}
|
|
1592
1586
|
if store is not None and sid is not None:
|
|
1593
1587
|
await store.upsert_background_task(
|
|
1594
|
-
sid, task_id=task_id, command=label, status="
|
|
1595
|
-
return_code=None, output_tail="(
|
|
1588
|
+
sid, task_id=task_id, command=label, status="queued",
|
|
1589
|
+
return_code=None, output_tail="(queued)",
|
|
1596
1590
|
)
|
|
1591
|
+
with self._lock:
|
|
1592
|
+
self.tasks[task_id] = {"command": label, "status": "queued", "result": None}
|
|
1593
|
+
# One FIFO admission gate per store/session/event loop. No await between lookup
|
|
1594
|
+
# and registration: concurrent submissions cannot oversubscribe the limit.
|
|
1595
|
+
key = (asyncio.get_running_loop(), id(store), sid)
|
|
1596
|
+
gate, users = self._tool_slots.get(key, (asyncio.Semaphore(8), 0))
|
|
1597
|
+
self._tool_slots[key] = (gate, users + 1)
|
|
1597
1598
|
|
|
1598
1599
|
# Its own ROOT stop token: a background task outlives the send that started it (the
|
|
1599
1600
|
# UI stop button stops the foreground, not this); only a stop by id reaches it.
|
|
@@ -1609,13 +1610,34 @@ class BackgroundManager:
|
|
|
1609
1610
|
async def _invoke() -> Any:
|
|
1610
1611
|
ctok = set_current_cancel_token(tok)
|
|
1611
1612
|
try:
|
|
1612
|
-
|
|
1613
|
+
async with gate:
|
|
1614
|
+
if store is not None and sid is not None:
|
|
1615
|
+
await store.upsert_background_task(
|
|
1616
|
+
sid, task_id=task_id, command=label, status="running",
|
|
1617
|
+
return_code=None, output_tail="(running)",
|
|
1618
|
+
)
|
|
1619
|
+
with self._lock:
|
|
1620
|
+
self.tasks[task_id]["status"] = "running"
|
|
1621
|
+
return await registry.invoke_async(name, args)
|
|
1613
1622
|
finally:
|
|
1614
1623
|
reset_current_cancel_token(ctok)
|
|
1615
1624
|
|
|
1616
|
-
self._supervise(task_id, label, store, sid, _invoke())
|
|
1625
|
+
worker = self._supervise(task_id, label, store, sid, _invoke())
|
|
1626
|
+
|
|
1627
|
+
def release_pool(_task: Any) -> None:
|
|
1628
|
+
semaphore, count = self._tool_slots[key]
|
|
1629
|
+
if count == 1:
|
|
1630
|
+
del self._tool_slots[key]
|
|
1631
|
+
else:
|
|
1632
|
+
self._tool_slots[key] = (semaphore, count - 1)
|
|
1633
|
+
|
|
1634
|
+
worker.add_done_callback(release_pool)
|
|
1635
|
+
# Start supervision before exposing the id, including for immediate cancellation.
|
|
1636
|
+
await asyncio.sleep(0)
|
|
1617
1637
|
return (
|
|
1618
|
-
f"
|
|
1638
|
+
f"后台任务已接收:task_id={task_id}({label[:80]})。"
|
|
1639
|
+
f"当前状态:{self.tasks[task_id]['status']}。"
|
|
1640
|
+
"同一会话最多同时执行 8 个后台工具,超出的自动排队,空出名额后自动执行,无需 check 释放名额。"
|
|
1619
1641
|
"**完成时结果会自动送到你面前,不用去取。** 现在去做别的;"
|
|
1620
1642
|
"手上没有别的可做就 pass_turn 停轮等它——用 check 反复问「好了没」只会空烧回合,"
|
|
1621
1643
|
"任务不会因此快一点,用户还得等你。"
|
|
@@ -1752,6 +1774,10 @@ class BackgroundManager:
|
|
|
1752
1774
|
if worker.done():
|
|
1753
1775
|
return "finished"
|
|
1754
1776
|
self._stopped.add(task_id)
|
|
1777
|
+
if self.tasks.get(task_id, {}).get("status") == "queued":
|
|
1778
|
+
worker.cancel()
|
|
1779
|
+
await asyncio.wait({worker}, timeout=5)
|
|
1780
|
+
return "stopped"
|
|
1755
1781
|
tok = self._stop_tokens.get(task_id)
|
|
1756
1782
|
if tok is not None and hasattr(tok, "cancel"):
|
|
1757
1783
|
tok.cancel("stopped")
|
|
@@ -1784,10 +1810,10 @@ class BackgroundManager:
|
|
|
1784
1810
|
ids = [str(x) for x in (task_ids or []) if str(x).strip()]
|
|
1785
1811
|
if not ids:
|
|
1786
1812
|
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)
|
|
1813
|
+
ids = [r.task_id for r in await store.list_background_tasks(sid) if str(r.status) in ("queued", "running")]
|
|
1788
1814
|
else:
|
|
1789
1815
|
with self._lock:
|
|
1790
|
-
ids = [tid for tid, tk in self.tasks.items() if tk.get("status")
|
|
1816
|
+
ids = [tid for tid, tk in self.tasks.items() if tk.get("status") in ("queued", "running")]
|
|
1791
1817
|
if not ids:
|
|
1792
1818
|
return "没有在跑的后台任务。"
|
|
1793
1819
|
timeout_s = max(1.0, min(float(timeout_s or 600.0), 3600.0))
|
|
@@ -1797,7 +1823,7 @@ class BackgroundManager:
|
|
|
1797
1823
|
end = loop.time() + timeout_s
|
|
1798
1824
|
while loop.time() < end:
|
|
1799
1825
|
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):
|
|
1826
|
+
if all(s not in ("queued", "running", "unknown") for s in states):
|
|
1801
1827
|
return
|
|
1802
1828
|
futs = {self._workers[tid] for tid in ids if tid in self._workers and not self._workers[tid].done()}
|
|
1803
1829
|
await asyncio.wait(futs or {asyncio.ensure_future(asyncio.sleep(1.0))}, timeout=1.0,
|
|
@@ -1810,7 +1836,7 @@ class BackgroundManager:
|
|
|
1810
1836
|
pending = 0
|
|
1811
1837
|
for tid in ids:
|
|
1812
1838
|
status, tail = await self._status_of(store, sid, tid)
|
|
1813
|
-
pending += status
|
|
1839
|
+
pending += status in ("queued", "running")
|
|
1814
1840
|
lines.append(f"{tid}: [{status}] {tail[-1500:] if tail else ''}".rstrip())
|
|
1815
1841
|
head = ("等的时候来了新消息,先停下来等(任务照常在跑)。" if steered
|
|
1816
1842
|
else f"还有 {pending} 件没做完(等了 {int(timeout_s)} 秒,任务照常在跑)。" if pending
|