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.
Files changed (62) hide show
  1. {power_loop-0.4.0 → power_loop-0.4.1}/PKG-INFO +1 -1
  2. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/__init__.py +3 -1
  3. power_loop-0.4.1/power_loop/agent/follow_up.py +61 -0
  4. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/stateful_loop.py +65 -2
  5. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/pipeline.py +8 -0
  6. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/PKG-INFO +1 -1
  7. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/SOURCES.txt +1 -0
  8. {power_loop-0.4.0 → power_loop-0.4.1}/LICENSE +0 -0
  9. {power_loop-0.4.0 → power_loop-0.4.1}/README.md +0 -0
  10. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/__init__.py +0 -0
  11. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/anthropic_factory.py +0 -0
  12. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/capabilities.py +0 -0
  13. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/interface.py +0 -0
  14. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/llm_factory.py +0 -0
  15. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/llm_tooling.py +0 -0
  16. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/llm_utils.py +0 -0
  17. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/multimodal.py +0 -0
  18. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/qwen_image.py +0 -0
  19. {power_loop-0.4.0 → power_loop-0.4.1}/llm_client/web_search.py +0 -0
  20. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/__init__.py +0 -0
  21. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/sink.py +0 -0
  22. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/system_prompt.py +0 -0
  23. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/agent/types.py +0 -0
  24. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/__init__.py +0 -0
  25. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/errors.py +0 -0
  26. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/event_payloads.py +0 -0
  27. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/events.py +0 -0
  28. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/handlers.py +0 -0
  29. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/hook_contexts.py +0 -0
  30. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/hooks.py +0 -0
  31. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/messages.py +0 -0
  32. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/protocols.py +0 -0
  33. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/contracts/tools.py +0 -0
  34. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/agent_context.py +0 -0
  35. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/events.py +0 -0
  36. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/hooks.py +0 -0
  37. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/phase.py +0 -0
  38. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/runner.py +0 -0
  39. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/core/state.py +0 -0
  40. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/budget.py +0 -0
  41. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/cancellation.py +0 -0
  42. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/compact.py +0 -0
  43. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/env.py +0 -0
  44. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/human_input.py +0 -0
  45. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/memory.py +0 -0
  46. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/provider.py +0 -0
  47. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/retry.py +0 -0
  48. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/runtime_state.py +0 -0
  49. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/session_store.py +0 -0
  50. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/skills.py +0 -0
  51. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/spec.py +0 -0
  52. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/runtime/structured.py +0 -0
  53. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/__init__.py +0 -0
  54. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/default_manifest.py +0 -0
  55. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/default_tools.py +0 -0
  56. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/registry.py +0 -0
  57. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop/tools/spawn_agent.py +0 -0
  58. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/dependency_links.txt +0 -0
  59. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/requires.txt +0 -0
  60. {power_loop-0.4.0 → power_loop-0.4.1}/power_loop.egg-info/top_level.txt +0 -0
  61. {power_loop-0.4.0 → power_loop-0.4.1}/pyproject.toml +0 -0
  62. {power_loop-0.4.0 → power_loop-0.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -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.0"
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -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