power-loop 6.2.0__tar.gz → 6.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {power_loop-6.2.0 → power_loop-6.3.0}/PKG-INFO +1 -1
  2. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/__init__.py +1 -1
  3. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/core/pipeline.py +12 -6
  4. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/image_recall.py +33 -11
  5. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop.egg-info/PKG-INFO +1 -1
  6. {power_loop-6.2.0 → power_loop-6.3.0}/LICENSE +0 -0
  7. {power_loop-6.2.0 → power_loop-6.3.0}/README.md +0 -0
  8. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/_vendor/__init__.py +0 -0
  9. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/_vendor/llm_client/__init__.py +0 -0
  10. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
  11. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/_vendor/llm_client/capabilities.py +0 -0
  12. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/_vendor/llm_client/interface.py +0 -0
  13. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/_vendor/llm_client/llm_factory.py +0 -0
  14. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
  15. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
  16. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/_vendor/llm_client/multimodal.py +0 -0
  17. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/agent/__init__.py +0 -0
  18. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/agent/follow_up.py +0 -0
  19. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/agent/sink.py +0 -0
  20. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/agent/stateful_loop.py +0 -0
  21. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/agent/system_prompt.py +0 -0
  22. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/agent/types.py +0 -0
  23. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/__init__.py +0 -0
  24. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/errors.py +0 -0
  25. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/event_payloads.py +0 -0
  26. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/events.py +0 -0
  27. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/handlers.py +0 -0
  28. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/hook_contexts.py +0 -0
  29. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/hooks.py +0 -0
  30. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/messages.py +0 -0
  31. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/protocols.py +0 -0
  32. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contracts/tools.py +0 -0
  33. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contrib/__init__.py +0 -0
  34. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contrib/_redact.py +0 -0
  35. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contrib/jsonl_sink.py +0 -0
  36. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contrib/logging_sink.py +0 -0
  37. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contrib/mcp.py +0 -0
  38. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contrib/metrics_sink.py +0 -0
  39. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/contrib/otel_sink.py +0 -0
  40. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/core/agent_context.py +0 -0
  41. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/core/events.py +0 -0
  42. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/core/hooks.py +0 -0
  43. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/core/phase.py +0 -0
  44. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/core/runner.py +0 -0
  45. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/core/state.py +0 -0
  46. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/py.typed +0 -0
  47. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/blackboard.py +0 -0
  48. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/budget.py +0 -0
  49. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/cancellation.py +0 -0
  50. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/compact.py +0 -0
  51. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/env.py +0 -0
  52. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/exec_backend.py +0 -0
  53. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/fold.py +0 -0
  54. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/fold_adapter.py +0 -0
  55. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/history_projector.py +0 -0
  56. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/history_sanitize.py +0 -0
  57. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/human_input.py +0 -0
  58. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/memory.py +0 -0
  59. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/notes.py +0 -0
  60. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/provider.py +0 -0
  61. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/representation.py +0 -0
  62. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/retry.py +0 -0
  63. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/runtime_state.py +0 -0
  64. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/session_store.py +0 -0
  65. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/skills.py +0 -0
  66. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/spec.py +0 -0
  67. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/__init__.py +0 -0
  68. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/backends/__init__.py +0 -0
  69. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/backends/mysql.py +0 -0
  70. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/backends/postgres.py +0 -0
  71. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/backends/sqlite.py +0 -0
  72. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/capabilities.py +0 -0
  73. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/db.py +0 -0
  74. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/dialect.py +0 -0
  75. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/factory.py +0 -0
  76. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/schema.py +0 -0
  77. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/store.py +0 -0
  78. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/store/types.py +0 -0
  79. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/structured.py +0 -0
  80. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/stub_provider.py +0 -0
  81. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/runtime/timers.py +0 -0
  82. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/tools/__init__.py +0 -0
  83. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/tools/blackboard.py +0 -0
  84. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/tools/command_policy.py +0 -0
  85. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/tools/default_manifest.py +0 -0
  86. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/tools/default_tools.py +0 -0
  87. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/tools/registry.py +0 -0
  88. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/tools/spawn_agent.py +0 -0
  89. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/__init__.py +0 -0
  90. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/api.py +0 -0
  91. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/engine.py +0 -0
  92. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/introspect.py +0 -0
  93. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/journal.py +0 -0
  94. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/result.py +0 -0
  95. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/resume.py +0 -0
  96. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/runner.py +0 -0
  97. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/spec.py +0 -0
  98. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/subprocess_executor.py +0 -0
  99. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/tool.py +0 -0
  100. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop/workflow/worker.py +0 -0
  101. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop.egg-info/SOURCES.txt +0 -0
  102. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop.egg-info/dependency_links.txt +0 -0
  103. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop.egg-info/requires.txt +0 -0
  104. {power_loop-6.2.0 → power_loop-6.3.0}/power_loop.egg-info/top_level.txt +0 -0
  105. {power_loop-6.2.0 → power_loop-6.3.0}/pyproject.toml +0 -0
  106. {power_loop-6.2.0 → power_loop-6.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 6.2.0
3
+ Version: 6.3.0
4
4
  Summary: Embeddable agent execution kernel — LLM loop, hooks, events, tools, dynamic sub-agents.
5
5
  Author-email: zhangran <zhangran24@126.com>
6
6
  License: MIT
@@ -15,7 +15,7 @@ Stability tiers
15
15
  无版本承诺,可随时变更或删除。
16
16
  """
17
17
 
18
- __version__ = "6.2.0"
18
+ __version__ = "6.3.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).
@@ -1044,12 +1044,18 @@ class AgentPipeline:
1044
1044
  await self._append_message(_pm, round_index=round_idx)
1045
1045
  llm_before.messages.append(dict(_pm))
1046
1046
 
1047
- # EPHEMERAL image recall: a tool (e.g. recall_send hitting an image row) can put a
1048
- # picture back in front of the model for exactly ONE round. Appended to the request
1049
- # only — never persisted, so it does not enter history or the projection. Durable
1050
- # would mean an agent that recalled three times carries three images forever, which
1051
- # is the unbounded growth the projection exists to prevent.
1052
- for _img in drain_queued_images(self.session_id):
1047
+ # Image recall: a tool (see_image / recall_send / an image generator) can put
1048
+ # pictures in front of the model. DURABLE ones become real `user` rows — the image
1049
+ # stays in view for the rest of the send (cheap: it sits in the provider's cached
1050
+ # prefix) and is distilled to `[image: … · file_uuid=…]` across sends, so nothing
1051
+ # accumulates without bound. EPHEMERAL ones go into this request only.
1052
+ # Appended AFTER the hook's own persist_messages so a tool's picture lands below
1053
+ # the tool result that announced it — which is exactly where the model expects it.
1054
+ _durable_imgs, _ephemeral_imgs = drain_queued_images(self.session_id)
1055
+ for _img in _durable_imgs:
1056
+ await self._append_message(_img, round_index=round_idx)
1057
+ llm_before.messages.append(dict(_img))
1058
+ for _img in _ephemeral_imgs:
1053
1059
  llm_before.messages.append(_img)
1054
1060
 
1055
1061
  if llm_before.directive == HookDirective.SHORT_CIRCUIT:
@@ -13,10 +13,16 @@ on which vendor the definition happens to point at, and definitions switch model
13
13
  So the image travels as its own **user** message instead, and the tool returns only a line
14
14
  saying it is there.
15
15
 
16
- Why EPHEMERAL: the queued message is appended to the round's request, never to the store. A
17
- durable injection would write the image into history and the projection, so an agent that
18
- recalled three times would carry three images forever — exactly the unbounded growth the
19
- projection exists to prevent. One round, then gone; recall again if you need another look.
16
+ DURABLE by default, and that default was reversed on purpose. The first cut was ephemeral —
17
+ request-only, gone next round — out of a fear that "recall three times and you carry three
18
+ images forever". Measurement killed that fear: the provider's prefix cache serves a stable
19
+ history at ~99% hit rate, so an image sitting in the prefix costs about a tenth of its face
20
+ value every later round. Meanwhile the SEMANTICS clearly favour durable: an agent that looked
21
+ at a UI screenshot and then spends fifteen rounds coding against it should still have the
22
+ picture in front of it, not have to ask for it again. Across sends the projection distils the
23
+ row down to `[image: shot.png · file_uuid=…]`, so nothing accumulates without bound.
24
+
25
+ Pass ``durable=False`` for the look-once case (request-only, never stored).
20
26
  """
21
27
 
22
28
  from __future__ import annotations
@@ -38,9 +44,14 @@ MAX_PENDING_PER_SESSION = 8
38
44
 
39
45
 
40
46
  def queue_image_for_next_round(
41
- session_id: str | None, *, path: str, note: str = "", ref: str = ""
47
+ session_id: str | None, *, path: str, note: str = "", ref: str = "",
48
+ durable: bool = True,
42
49
  ) -> bool:
43
- """Queue one image (by local path) to be shown to the model on the next round.
50
+ """Queue one image (by local path) to be shown to the model from the next round on.
51
+
52
+ ``durable=True`` (default) stores it as a real ``user`` row, so it stays in front of the
53
+ model for the rest of the send and is distilled to a text reference across sends.
54
+ ``durable=False`` puts it in the request only — visible for exactly one round.
44
55
 
45
56
  Returns False when there is no session to queue against, or the queue is full — the caller
46
57
  should then say so in its own return value rather than pretend the image was delivered.
@@ -62,16 +73,27 @@ def queue_image_for_next_round(
62
73
  queue = _pending.setdefault(session_id, [])
63
74
  if len(queue) >= MAX_PENDING_PER_SESSION:
64
75
  return False
65
- queue.append({"role": "user", "content": blocks})
76
+ queue.append({"role": "user", "content": blocks, "__durable__": durable})
66
77
  return True
67
78
 
68
79
 
69
- def drain_queued_images(session_id: str | None) -> list[LoopMessage]:
70
- """Take everything queued for this session (and clear it)."""
80
+ def drain_queued_images(session_id: str | None) -> tuple[list[LoopMessage], list[LoopMessage]]:
81
+ """Take everything queued for this session (and clear it).
82
+
83
+ Returns ``(durable, ephemeral)`` — the caller persists the first group and appends the
84
+ second to this round's request only. The marker key is stripped so neither ever reaches
85
+ a provider payload.
86
+ """
71
87
  if not session_id:
72
- return []
88
+ return [], []
73
89
  with _lock:
74
- return _pending.pop(session_id, [])
90
+ queued = _pending.pop(session_id, [])
91
+ durable: list[LoopMessage] = []
92
+ ephemeral: list[LoopMessage] = []
93
+ for item in queued:
94
+ keep = bool(item.pop("__durable__", True))
95
+ (durable if keep else ephemeral).append(item)
96
+ return durable, ephemeral
75
97
 
76
98
 
77
99
  def discard_queued_images(session_id: str | None) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 6.2.0
3
+ Version: 6.3.0
4
4
  Summary: Embeddable agent execution kernel — LLM loop, hooks, events, tools, dynamic sub-agents.
5
5
  Author-email: zhangran <zhangran24@126.com>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes