power-loop 0.4.0__tar.gz → 0.4.1__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-0.4.0 → power_loop-0.4.1}/PKG-INFO +1 -1
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/__init__.py +3 -1
- power_loop-0.4.1/power_loop/agent/follow_up.py +61 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/stateful_loop.py +65 -2
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/pipeline.py +8 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/PKG-INFO +1 -1
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/SOURCES.txt +1 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/LICENSE +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/README.md +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/__init__.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/anthropic_factory.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/capabilities.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/interface.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/llm_factory.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/llm_tooling.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/llm_utils.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/multimodal.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/qwen_image.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/web_search.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/__init__.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/sink.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/system_prompt.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/types.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/__init__.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/errors.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/event_payloads.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/events.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/handlers.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/hook_contexts.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/hooks.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/messages.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/protocols.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/tools.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/agent_context.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/events.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/hooks.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/phase.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/runner.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/state.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/budget.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/cancellation.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/compact.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/env.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/human_input.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/memory.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/provider.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/retry.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/runtime_state.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/session_store.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/skills.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/spec.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/structured.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/__init__.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/default_manifest.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/default_tools.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/registry.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/spawn_agent.py +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/dependency_links.txt +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/requires.txt +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/top_level.txt +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/pyproject.toml +0 -0
- {power_loop-0.4.0 → power_loop-0.4.1}/setup.cfg +0 -0
|
@@ -7,6 +7,7 @@ Stability tiers
|
|
|
7
7
|
业务方(如 DeepTalk `agent` 服务)只应依赖这一层。
|
|
8
8
|
|
|
9
9
|
StatefulAgentLoop, StatefulResult,
|
|
10
|
+
FollowUpQueued,
|
|
10
11
|
AgentLoopConfig, AgentLoopResult,
|
|
11
12
|
SessionStore,
|
|
12
13
|
PowerLoopError,
|
|
@@ -21,10 +22,11 @@ Stability tiers
|
|
|
21
22
|
无版本承诺,可随时变更或删除。
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
|
-
__version__ = "0.4.
|
|
25
|
+
__version__ = "0.4.1"
|
|
25
26
|
|
|
26
27
|
from power_loop.agent.sink import MessageSink, NullSink, SQLiteSink
|
|
27
28
|
from power_loop.agent.stateful_loop import StatefulAgentLoop, StatefulResult
|
|
29
|
+
from power_loop.agent.follow_up import FollowUpQueued
|
|
28
30
|
from power_loop.agent.system_prompt import (
|
|
29
31
|
BUILTIN_SECTIONS,
|
|
30
32
|
DEFAULT_AGENT_SYSTEM_PROMPT,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Follow-up / steering queue for in-flight agent loops."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from power_loop.agent.types import LoopMessage
|
|
9
|
+
|
|
10
|
+
FOLLOW_UP_MESSAGE_NAME = "follow_up"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class FollowUpQueued:
|
|
15
|
+
"""Returned when :meth:`StatefulAgentLoop.follow_up` enqueues steering input."""
|
|
16
|
+
|
|
17
|
+
session_id: str
|
|
18
|
+
queue_depth: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_follow_up_user_message(text: str) -> LoopMessage:
|
|
22
|
+
"""Wrap steering text as a user message for the LLM transcript."""
|
|
23
|
+
body = text.strip()
|
|
24
|
+
return {
|
|
25
|
+
"role": "user",
|
|
26
|
+
"name": FOLLOW_UP_MESSAGE_NAME,
|
|
27
|
+
"content": f"<follow_up>\n{body}\n</follow_up>",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def merge_follow_up_inputs(items: list[str | LoopMessage]) -> LoopMessage | None:
|
|
32
|
+
"""Merge queued follow-up payloads into one user message."""
|
|
33
|
+
parts: list[str] = []
|
|
34
|
+
for item in items:
|
|
35
|
+
if isinstance(item, str):
|
|
36
|
+
text = item.strip()
|
|
37
|
+
else:
|
|
38
|
+
text = _content_as_text(item.get("content")).strip()
|
|
39
|
+
if text:
|
|
40
|
+
parts.append(text)
|
|
41
|
+
if not parts:
|
|
42
|
+
return None
|
|
43
|
+
return format_follow_up_user_message("\n\n".join(parts))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _content_as_text(content: Any) -> str:
|
|
47
|
+
if content is None:
|
|
48
|
+
return ""
|
|
49
|
+
if isinstance(content, str):
|
|
50
|
+
return content
|
|
51
|
+
import json
|
|
52
|
+
|
|
53
|
+
return json.dumps(content, ensure_ascii=False)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"FOLLOW_UP_MESSAGE_NAME",
|
|
58
|
+
"FollowUpQueued",
|
|
59
|
+
"format_follow_up_user_message",
|
|
60
|
+
"merge_follow_up_inputs",
|
|
61
|
+
]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""StatefulAgentLoop — the single public entry point for power-loop.
|
|
2
2
|
|
|
3
3
|
Owns a :class:`SessionStore` and gives callers a stateful,
|
|
4
|
-
``new_session()`` + ``send(user_input, session_id=...)`` interface
|
|
4
|
+
``new_session()`` + ``send(user_input, session_id=...)`` interface, plus
|
|
5
|
+
:meth:`follow_up` for steering an in-flight loop without blocking.
|
|
5
6
|
Everything else — pipeline orchestration, hooks, events, tool invocation,
|
|
6
7
|
persistence, pending-state machine — is wired up internally.
|
|
7
8
|
|
|
@@ -23,6 +24,7 @@ from dataclasses import dataclass, field, replace
|
|
|
23
24
|
from typing import Any
|
|
24
25
|
|
|
25
26
|
from llm_client.interface import LLMService
|
|
27
|
+
from power_loop.agent.follow_up import FollowUpQueued, merge_follow_up_inputs
|
|
26
28
|
from power_loop.agent.sink import SQLiteSink
|
|
27
29
|
from power_loop.agent.system_prompt import (
|
|
28
30
|
DEFAULT_AGENT_SYSTEM_PROMPT,
|
|
@@ -93,6 +95,8 @@ class StatefulAgentLoop:
|
|
|
93
95
|
self.tool_registry = tool_registry
|
|
94
96
|
self._runner = AgentRunner(event_bus=event_bus, hooks=hooks)
|
|
95
97
|
self._locks: dict[str, asyncio.Lock] = {}
|
|
98
|
+
self._follow_up_queues: dict[str, list[str | LoopMessage]] = {}
|
|
99
|
+
self._follow_up_queue_locks: dict[str, asyncio.Lock] = {}
|
|
96
100
|
|
|
97
101
|
# ── lifecycle ─────────────────────────────────────────────────────────
|
|
98
102
|
|
|
@@ -144,6 +148,42 @@ class StatefulAgentLoop:
|
|
|
144
148
|
self._persist_user_input(sid, user_input)
|
|
145
149
|
return await self._run_loop(sid, stop_event=stop_event)
|
|
146
150
|
|
|
151
|
+
async def follow_up(
|
|
152
|
+
self,
|
|
153
|
+
user_input: str | LoopMessage,
|
|
154
|
+
session_id: str,
|
|
155
|
+
*,
|
|
156
|
+
stop_event: CancellationLike = None,
|
|
157
|
+
) -> StatefulResult | FollowUpQueued:
|
|
158
|
+
"""Steer an in-flight loop, or fall back to :meth:`send`.
|
|
159
|
+
|
|
160
|
+
When the session lock is held by a running :meth:`send` / :meth:`resume`
|
|
161
|
+
/ :meth:`submit_input`, the input is appended to a per-session queue.
|
|
162
|
+
The pipeline drains that queue at each **round** boundary (before
|
|
163
|
+
``prepare_round``), injects a wrapped ``<follow_up>`` user message, and
|
|
164
|
+
clears the drained items.
|
|
165
|
+
|
|
166
|
+
When the session is idle (lock not held), behaves like :meth:`send`.
|
|
167
|
+
"""
|
|
168
|
+
sid = session_id
|
|
169
|
+
self._ensure_session_or_raise(sid)
|
|
170
|
+
session_lock = self._lock_for(sid)
|
|
171
|
+
if session_lock.locked():
|
|
172
|
+
depth = await self._enqueue_follow_up(sid, user_input)
|
|
173
|
+
return FollowUpQueued(session_id=sid, queue_depth=depth)
|
|
174
|
+
return await self.send(user_input, sid, stop_event=stop_event)
|
|
175
|
+
|
|
176
|
+
def follow_up_sync(
|
|
177
|
+
self,
|
|
178
|
+
user_input: str | LoopMessage,
|
|
179
|
+
session_id: str,
|
|
180
|
+
*,
|
|
181
|
+
stop_event: CancellationLike = None,
|
|
182
|
+
) -> StatefulResult | FollowUpQueued:
|
|
183
|
+
return asyncio.run(
|
|
184
|
+
self.follow_up(user_input, session_id, stop_event=stop_event)
|
|
185
|
+
)
|
|
186
|
+
|
|
147
187
|
def send_sync(
|
|
148
188
|
self,
|
|
149
189
|
user_input: str | LoopMessage,
|
|
@@ -365,6 +405,25 @@ class StatefulAgentLoop:
|
|
|
365
405
|
self._locks[sid] = lock
|
|
366
406
|
return lock
|
|
367
407
|
|
|
408
|
+
def _follow_up_queue_lock_for(self, sid: str) -> asyncio.Lock:
|
|
409
|
+
lock = self._follow_up_queue_locks.get(sid)
|
|
410
|
+
if lock is None:
|
|
411
|
+
lock = asyncio.Lock()
|
|
412
|
+
self._follow_up_queue_locks[sid] = lock
|
|
413
|
+
return lock
|
|
414
|
+
|
|
415
|
+
async def _enqueue_follow_up(self, sid: str, user_input: str | LoopMessage) -> int:
|
|
416
|
+
async with self._follow_up_queue_lock_for(sid):
|
|
417
|
+
queue = self._follow_up_queues.setdefault(sid, [])
|
|
418
|
+
queue.append(user_input)
|
|
419
|
+
return len(queue)
|
|
420
|
+
|
|
421
|
+
async def _drain_follow_up_messages(self, sid: str) -> list[LoopMessage]:
|
|
422
|
+
async with self._follow_up_queue_lock_for(sid):
|
|
423
|
+
pending = self._follow_up_queues.pop(sid, [])
|
|
424
|
+
merged = merge_follow_up_inputs(pending)
|
|
425
|
+
return [merged] if merged is not None else []
|
|
426
|
+
|
|
368
427
|
def _ensure_session_or_raise(self, sid: str) -> None:
|
|
369
428
|
if self.store.get_session(sid) is None:
|
|
370
429
|
raise SessionNotFoundError(sid)
|
|
@@ -457,6 +516,9 @@ class StatefulAgentLoop:
|
|
|
457
516
|
async with self._runner.session_async(session_id=sid):
|
|
458
517
|
loop_token = set_current_loop(self)
|
|
459
518
|
try:
|
|
519
|
+
async def _drain_follow_ups() -> list[LoopMessage]:
|
|
520
|
+
return await self._drain_follow_up_messages(sid)
|
|
521
|
+
|
|
460
522
|
pipeline = AgentPipeline(
|
|
461
523
|
llm=self.llm,
|
|
462
524
|
config=runtime_config,
|
|
@@ -468,6 +530,7 @@ class StatefulAgentLoop:
|
|
|
468
530
|
stop_event=stop_event,
|
|
469
531
|
sink=sink,
|
|
470
532
|
store=self.store,
|
|
533
|
+
drain_follow_ups=_drain_follow_ups,
|
|
471
534
|
)
|
|
472
535
|
result: AgentLoopResult = await pipeline.run(history)
|
|
473
536
|
finally:
|
|
@@ -555,4 +618,4 @@ def _as_tool_result_text(value: Any) -> str:
|
|
|
555
618
|
return json.dumps(value, ensure_ascii=False)
|
|
556
619
|
|
|
557
620
|
|
|
558
|
-
__all__ = ["StatefulAgentLoop", "StatefulResult", "MessageState"]
|
|
621
|
+
__all__ = ["StatefulAgentLoop", "StatefulResult", "MessageState", "FollowUpQueued"]
|
|
@@ -201,6 +201,7 @@ class AgentPipeline:
|
|
|
201
201
|
stop_event: CancellationLike = None,
|
|
202
202
|
sink: MessageSink | None = None,
|
|
203
203
|
store: Any | None = None,
|
|
204
|
+
drain_follow_ups: Any | None = None,
|
|
204
205
|
) -> None:
|
|
205
206
|
self.llm = llm
|
|
206
207
|
self.config = config
|
|
@@ -215,6 +216,7 @@ class AgentPipeline:
|
|
|
215
216
|
self.stop_event = stop_event if isinstance(stop_event, threading.Event) else None
|
|
216
217
|
self.sink: MessageSink = sink if sink is not None else NullSink()
|
|
217
218
|
self.store = store
|
|
219
|
+
self._drain_follow_ups = drain_follow_ups
|
|
218
220
|
|
|
219
221
|
base_prompt = (config.system_prompt or DEFAULT_AGENT_SYSTEM_PROMPT).strip()
|
|
220
222
|
self.runtime_tools = tool_registry.to_openai_tools() if tool_registry is not None else None
|
|
@@ -639,6 +641,12 @@ class AgentPipeline:
|
|
|
639
641
|
if isinstance(round_ctx.messages, list):
|
|
640
642
|
self.history = round_ctx.messages
|
|
641
643
|
|
|
644
|
+
# ── In-flight steering: drain follow-up queue at round boundary ──
|
|
645
|
+
if self._drain_follow_ups is not None:
|
|
646
|
+
drained = await self._drain_follow_ups()
|
|
647
|
+
for msg in drained:
|
|
648
|
+
await self._append_message(msg, round_index=round_idx)
|
|
649
|
+
|
|
642
650
|
# ── Business logic: prepare round ──
|
|
643
651
|
await self.prepare_round(round_idx)
|
|
644
652
|
|
|
@@ -18,6 +18,7 @@ power_loop.egg-info/dependency_links.txt
|
|
|
18
18
|
power_loop.egg-info/requires.txt
|
|
19
19
|
power_loop.egg-info/top_level.txt
|
|
20
20
|
power_loop/agent/__init__.py
|
|
21
|
+
power_loop/agent/follow_up.py
|
|
21
22
|
power_loop/agent/sink.py
|
|
22
23
|
power_loop/agent/stateful_loop.py
|
|
23
24
|
power_loop/agent/system_prompt.py
|
|
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
|