power-loop 3.2.0__tar.gz → 3.4.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 (104) hide show
  1. {power_loop-3.2.0 → power_loop-3.4.0}/PKG-INFO +1 -1
  2. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/__init__.py +3 -1
  3. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/representation.py +111 -33
  4. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop.egg-info/PKG-INFO +1 -1
  5. {power_loop-3.2.0 → power_loop-3.4.0}/LICENSE +0 -0
  6. {power_loop-3.2.0 → power_loop-3.4.0}/README.md +0 -0
  7. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/_vendor/__init__.py +0 -0
  8. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/_vendor/llm_client/__init__.py +0 -0
  9. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
  10. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/_vendor/llm_client/capabilities.py +0 -0
  11. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/_vendor/llm_client/interface.py +0 -0
  12. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/_vendor/llm_client/llm_factory.py +0 -0
  13. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
  14. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
  15. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/_vendor/llm_client/multimodal.py +0 -0
  16. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/agent/__init__.py +0 -0
  17. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/agent/follow_up.py +0 -0
  18. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/agent/sink.py +0 -0
  19. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/agent/stateful_loop.py +0 -0
  20. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/agent/system_prompt.py +0 -0
  21. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/agent/types.py +0 -0
  22. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/__init__.py +0 -0
  23. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/errors.py +0 -0
  24. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/event_payloads.py +0 -0
  25. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/events.py +0 -0
  26. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/handlers.py +0 -0
  27. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/hook_contexts.py +0 -0
  28. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/hooks.py +0 -0
  29. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/messages.py +0 -0
  30. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/protocols.py +0 -0
  31. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contracts/tools.py +0 -0
  32. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contrib/__init__.py +0 -0
  33. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contrib/_redact.py +0 -0
  34. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contrib/jsonl_sink.py +0 -0
  35. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contrib/logging_sink.py +0 -0
  36. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contrib/mcp.py +0 -0
  37. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contrib/metrics_sink.py +0 -0
  38. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/contrib/otel_sink.py +0 -0
  39. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/core/agent_context.py +0 -0
  40. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/core/events.py +0 -0
  41. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/core/hooks.py +0 -0
  42. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/core/phase.py +0 -0
  43. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/core/pipeline.py +0 -0
  44. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/core/runner.py +0 -0
  45. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/core/state.py +0 -0
  46. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/py.typed +0 -0
  47. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/blackboard.py +0 -0
  48. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/budget.py +0 -0
  49. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/cancellation.py +0 -0
  50. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/compact.py +0 -0
  51. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/env.py +0 -0
  52. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/exec_backend.py +0 -0
  53. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/fold.py +0 -0
  54. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/fold_adapter.py +0 -0
  55. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/history_projector.py +0 -0
  56. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/history_sanitize.py +0 -0
  57. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/human_input.py +0 -0
  58. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/memory.py +0 -0
  59. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/notes.py +0 -0
  60. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/provider.py +0 -0
  61. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/retry.py +0 -0
  62. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/runtime_state.py +0 -0
  63. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/session_store.py +0 -0
  64. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/skills.py +0 -0
  65. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/spec.py +0 -0
  66. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/__init__.py +0 -0
  67. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/backends/__init__.py +0 -0
  68. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/backends/mysql.py +0 -0
  69. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/backends/postgres.py +0 -0
  70. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/backends/sqlite.py +0 -0
  71. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/capabilities.py +0 -0
  72. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/db.py +0 -0
  73. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/dialect.py +0 -0
  74. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/factory.py +0 -0
  75. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/schema.py +0 -0
  76. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/store.py +0 -0
  77. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/store/types.py +0 -0
  78. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/structured.py +0 -0
  79. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/stub_provider.py +0 -0
  80. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/runtime/timers.py +0 -0
  81. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/tools/__init__.py +0 -0
  82. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/tools/blackboard.py +0 -0
  83. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/tools/default_manifest.py +0 -0
  84. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/tools/default_tools.py +0 -0
  85. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/tools/registry.py +0 -0
  86. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/tools/spawn_agent.py +0 -0
  87. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/__init__.py +0 -0
  88. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/api.py +0 -0
  89. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/engine.py +0 -0
  90. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/introspect.py +0 -0
  91. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/journal.py +0 -0
  92. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/result.py +0 -0
  93. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/resume.py +0 -0
  94. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/runner.py +0 -0
  95. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/spec.py +0 -0
  96. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/subprocess_executor.py +0 -0
  97. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/tool.py +0 -0
  98. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop/workflow/worker.py +0 -0
  99. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop.egg-info/SOURCES.txt +0 -0
  100. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop.egg-info/dependency_links.txt +0 -0
  101. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop.egg-info/requires.txt +0 -0
  102. {power_loop-3.2.0 → power_loop-3.4.0}/power_loop.egg-info/top_level.txt +0 -0
  103. {power_loop-3.2.0 → power_loop-3.4.0}/pyproject.toml +0 -0
  104. {power_loop-3.2.0 → power_loop-3.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 3.2.0
3
+ Version: 3.4.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__ = "3.2.0"
18
+ __version__ = "3.4.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).
@@ -177,6 +177,7 @@ from power_loop.runtime.representation import (
177
177
  ProjectedRepresentation,
178
178
  ProjectedRow,
179
179
  ProjectedSend,
180
+ ProjectionRenderConfig,
180
181
  Representation,
181
182
  VerbatimRepresentation,
182
183
  )
@@ -338,6 +339,7 @@ __all__ = [
338
339
  "Representation",
339
340
  "VerbatimRepresentation",
340
341
  "ProjectedRepresentation",
342
+ "ProjectionRenderConfig",
341
343
  "FoldStrategy",
342
344
  "FoldContext",
343
345
  "FoldResult",
@@ -23,6 +23,7 @@ from __future__ import annotations
23
23
  import json
24
24
  from collections import deque
25
25
  from dataclasses import dataclass, field
26
+ from dataclasses import fields as dataclass_fields
26
27
  from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
27
28
 
28
29
  from power_loop.runtime.store.types import MessageRow, ProjectMessageRow
@@ -40,6 +41,7 @@ __all__ = [
40
41
  "Representation",
41
42
  "VerbatimRepresentation",
42
43
  "ProjectedRepresentation",
44
+ "ProjectionRenderConfig",
43
45
  "ProjectedRow",
44
46
  "ProjectedSend",
45
47
  ]
@@ -175,21 +177,60 @@ class VerbatimRepresentation:
175
177
  # ── Projection (generic structured summary; no LLM) ─────────────────────────────
176
178
 
177
179
 
180
+ @dataclass
181
+ class ProjectionRenderConfig:
182
+ """Tunable format knobs for :meth:`ProjectedRepresentation.render` — they change the SHAPE of the
183
+ rendered text only, never what is stored. Every field is a plain scalar so the whole config
184
+ round-trips through JSON (a host can surface it in an admin UI and retune the rendered context
185
+ live, then pass it back via ``ProjectedRepresentation(render_config=…)``). Placeholders: ``{n}`` in
186
+ a tag → the send_index (empty tag or ``None`` index → no tag); ``{range}`` in ``fold_note`` → the
187
+ folded ``#lo–#hi`` span. The defaults reproduce the built-in rendering exactly."""
188
+
189
+ user_tag: str = "[#{n}] " # prefix on a user/input row; {n}=send_index
190
+ project_tag: str = "#{n} " # prefix on a project/assistant row
191
+ tools_header: str = "[tools] " # before the tool list in a project row
192
+ tool_sep: str = "; " # between tools
193
+ tool_arg_sep: str = ", " # between a tool's k=v fields
194
+ include_tools: bool = True # render the tool list at all
195
+ include_final_text: bool = True # render the project row's trailing final_text
196
+ empty_project: str = "(no output)" # a project row that renders to nothing
197
+ fold_note: str = "[older sends {range} folded — recall_send(send_index=N) to expand]"
198
+
199
+ @classmethod
200
+ def from_dict(cls, data: dict[str, Any] | None) -> ProjectionRenderConfig:
201
+ """Build from a plain dict (e.g. JSON config), silently ignoring unknown keys."""
202
+ if not data:
203
+ return cls()
204
+ names = {f.name for f in dataclass_fields(cls)}
205
+ return cls(**{k: v for k, v in data.items() if k in names})
206
+
207
+
178
208
  @dataclass
179
209
  class ProjectedRepresentation:
180
210
  """Generic, deterministic, no-LLM per-send projection. Each send →
181
- ``user`` row: ``{"human": [<user inputs>]}`` (a LIST — folded follow-ups preserved) +
211
+ ``user`` row: ``{"input": [<user/trigger inputs, verbatim>]}`` (a LIST — folded follow-ups
212
+ preserved; pre-3.3 rows used the key ``human``) +
182
213
  ``project`` row: ``{"tools": [...], "final_text": ...}``. Each tool call is summarized via its
183
214
  ``ToolDefinition.project`` hook when present, else a truncating fallback. Rendered to terse
184
215
  plain text with NO tool-protocol structure. (This is the old ``DefaultDeterministicProjector``
185
- MINUS its fold knobs/``compact()`` — folding now lives on :class:`FoldStrategy`.)"""
216
+ MINUS its fold knobs/``compact()`` — folding now lives on :class:`FoldStrategy`.)
217
+
218
+ The render is customizable two ways without copy-pasting it: pass a :class:`ProjectionRenderConfig`
219
+ (``render_config=`` — JSON-friendly format knobs, retunable from config/an admin UI) and/or
220
+ override one of the small per-row methods (``render_row`` → ``render_user_row`` /
221
+ ``render_project_row`` / ``render_compact_row``). Defaults reproduce the prior output exactly."""
186
222
 
187
223
  kind: str = "projection"
188
224
  version: int = 1
189
225
  max_chars: int = 300 # per-field truncation budget
226
+ #: Format knobs for render() — see :class:`ProjectionRenderConfig`. A plain dict is coerced (so a
227
+ #: host can pass JSON config straight through). Subclasses can ALSO override the render_* methods.
228
+ render_config: ProjectionRenderConfig = field(default_factory=ProjectionRenderConfig)
190
229
 
191
230
  def __post_init__(self) -> None:
192
231
  _validate_representation_params(version=self.version, max_chars=self.max_chars)
232
+ if isinstance(self.render_config, dict):
233
+ self.render_config = ProjectionRenderConfig.from_dict(self.render_config)
193
234
 
194
235
  def project_send(
195
236
  self, send_rows: list[MessageRow], *, send_index: int, tool_registry: ToolRegistry | None
@@ -220,9 +261,13 @@ class ProjectedRepresentation:
220
261
  seqs = [r.seq for r in send_rows]
221
262
  rows: list[ProjectedRow] = []
222
263
  if users:
223
- rows.append(
224
- ProjectedRow("user", {"human": [_truncate(u.content, self.max_chars) for u in users]})
225
- )
264
+ # The INPUT side of a send (the user/trigger turn) is kept VERBATIM — it is the actual
265
+ # conversation content, it is short relative to tool output, and truncating it would drop
266
+ # context the model genuinely needs. Only the assistant's WORK (tool args/results +
267
+ # final_text) is compressed, which is where the token savings actually are. Key is
268
+ # ``input`` (the input turn — not necessarily a human; a multi-agent host feeds another
269
+ # agent's message here); pre-3.3 rows used ``human`` and are still read (see render()).
270
+ rows.append(ProjectedRow("user", {"input": [u.content for u in users]}))
226
271
  rows.append(
227
272
  ProjectedRow(
228
273
  "project",
@@ -259,50 +304,83 @@ class ProjectedRepresentation:
259
304
  return {"name": name, "result": _truncate(result_str, self.max_chars)}
260
305
 
261
306
  # rendering ----------------------------------------------------------------
307
+ # Orchestration only; the per-row shapes live in small overridable methods (override exactly the
308
+ # one you want instead of copy-pasting render) and the format lives in render_config (retune from
309
+ # config, no code). Each rendered send is tagged with its ``#N`` send_index so the model can call
310
+ # recall_send(send_index=N) on a folded earlier turn — the tool docstring + the host's
311
+ # RECALL_SEND_NOTE tell it to use "the #N the summary shows", so the tags MUST be emitted (else
312
+ # recall_send is undiscoverable); changing user_tag/project_tag away from a ``{n}`` form is at the
313
+ # host's own risk.
262
314
  def render(self, rows: list[ProjectMessageRow]) -> list[LoopMessage]:
263
- # Each rendered send is tagged with its ``#N`` send_index so the model can call
264
- # recall_send(send_index=N) on a folded/compacted earlier turn — the tool docstring and the
265
- # host's RECALL_SEND_NOTE both tell it to use "the #N the summary shows", so render MUST
266
- # actually emit them (else recall_send is undiscoverable). The folded compact carries its
267
- # covered range.
268
315
  out: list[LoopMessage] = []
269
316
  for r in rows:
270
- si = r.send_index
271
- if r.kind == "user":
272
- humans = (r.content or {}).get("human") or []
273
- tag = f"[#{si}] " if si is not None else ""
274
- out.append({"role": "user", "content": tag + "\n".join(str(h) for h in humans)})
275
- elif r.kind == "project":
276
- tag = f"#{si} " if si is not None else ""
277
- out.append({"role": "assistant", "content": tag + self._render_project(r.content)})
278
- elif r.kind == "compact":
279
- msg = _render_compact_row(r)
280
- lo, hi = r.compact_from_send, r.compact_to_send
281
- if lo is not None and hi is not None and hi >= lo > 0:
282
- rng = f"#{lo}" if lo == hi else f"#{lo}–#{hi}"
283
- msg = {
284
- "role": "user",
285
- "content": f"[older sends {rng} folded — recall_send(send_index=N) to "
286
- f"expand]\n{msg['content']}",
287
- }
317
+ msg = self.render_row(r)
318
+ if msg is not None:
288
319
  out.append(msg)
289
320
  return out
290
321
 
322
+ def render_row(self, r: ProjectMessageRow) -> LoopMessage | None:
323
+ """Dispatch one stored row to its per-kind renderer. Override to add a kind / change routing;
324
+ an unhandled kind returns None (skipped)."""
325
+ if r.kind == "user":
326
+ return self.render_user_row(r)
327
+ if r.kind == "project":
328
+ return self.render_project_row(r)
329
+ if r.kind == "compact":
330
+ return self.render_compact_row(r)
331
+ return None
332
+
333
+ def render_user_row(self, r: ProjectMessageRow) -> LoopMessage:
334
+ content = r.content or {}
335
+ # ``input`` since 3.3; ``human`` is the pre-3.3 key — read both so old rows still render.
336
+ inputs = content.get("input")
337
+ if inputs is None:
338
+ inputs = content.get("human") or []
339
+ return {
340
+ "role": "user",
341
+ "content": self._send_tag(self.render_config.user_tag, r.send_index)
342
+ + "\n".join(str(h) for h in inputs),
343
+ }
344
+
345
+ def render_project_row(self, r: ProjectMessageRow) -> LoopMessage:
346
+ return {
347
+ "role": "assistant",
348
+ "content": self._send_tag(self.render_config.project_tag, r.send_index)
349
+ + self._render_project(r.content),
350
+ }
351
+
352
+ def render_compact_row(self, r: ProjectMessageRow) -> LoopMessage:
353
+ msg = _render_compact_row(r)
354
+ lo, hi = r.compact_from_send, r.compact_to_send
355
+ if lo is not None and hi is not None and hi >= lo > 0:
356
+ rng = f"#{lo}" if lo == hi else f"#{lo}–#{hi}"
357
+ note = self.render_config.fold_note.replace("{range}", rng)
358
+ return {"role": "user", "content": f"{note}\n{msg['content']}"}
359
+ return msg
360
+
361
+ @staticmethod
362
+ def _send_tag(template: str, send_index: int | None) -> str:
363
+ """A send_index tag from a ``{n}`` template; empty template or ``None`` index → no tag."""
364
+ if send_index is None or not template:
365
+ return ""
366
+ return template.replace("{n}", str(send_index))
367
+
291
368
  def _render_tool(self, t: dict[str, Any]) -> str:
292
369
  name = t.get("name", "?")
293
370
  rest = {k: v for k, v in (t or {}).items() if k != "name"}
294
371
  if not rest:
295
372
  return str(name)
296
- body = ", ".join(f"{k}={v}" for k, v in rest.items())
373
+ body = self.render_config.tool_arg_sep.join(f"{k}={v}" for k, v in rest.items())
297
374
  return f"{name}({body})"
298
375
 
299
376
  def _render_project(self, content: dict[str, Any] | None) -> str:
300
377
  content = content or {}
378
+ cfg = self.render_config
301
379
  parts: list[str] = []
302
380
  tools = content.get("tools") or []
303
- if tools:
304
- parts.append("[tools] " + "; ".join(self._render_tool(t) for t in tools))
381
+ if tools and cfg.include_tools:
382
+ parts.append(cfg.tools_header + cfg.tool_sep.join(self._render_tool(t) for t in tools))
305
383
  ft = content.get("final_text")
306
- if ft:
384
+ if ft and cfg.include_final_text:
307
385
  parts.append(str(ft))
308
- return "\n".join(parts) if parts else "(no output)"
386
+ return "\n".join(parts) if parts else cfg.empty_project
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: power-loop
3
- Version: 3.2.0
3
+ Version: 3.4.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