power-loop 0.8.0__tar.gz → 0.9.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-0.8.0/power_loop.egg-info → power_loop-0.9.0}/PKG-INFO +38 -1
- {power_loop-0.8.0 → power_loop-0.9.0}/README.md +37 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/__init__.py +1 -1
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/agent/stateful_loop.py +33 -3
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/agent/types.py +4 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/errors.py +4 -1
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/core/pipeline.py +1 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/core/state.py +12 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/notes.py +6 -1
- {power_loop-0.8.0 → power_loop-0.9.0/power_loop.egg-info}/PKG-INFO +38 -1
- {power_loop-0.8.0 → power_loop-0.9.0}/LICENSE +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/__init__.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/anthropic_factory.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/capabilities.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/interface.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/llm_factory.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/llm_tooling.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/llm_utils.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/multimodal.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/qwen_image.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/llm_client/web_search.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/agent/__init__.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/agent/follow_up.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/agent/sink.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/agent/system_prompt.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/__init__.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/event_payloads.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/events.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/handlers.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/hook_contexts.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/hooks.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/messages.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/protocols.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/contracts/tools.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/core/agent_context.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/core/events.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/core/hooks.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/core/phase.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/core/runner.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/budget.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/cancellation.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/compact.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/env.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/exec_backend.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/human_input.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/memory.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/provider.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/retry.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/runtime_state.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/session_store.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/skills.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/spec.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/runtime/structured.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/tools/__init__.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/tools/default_manifest.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/tools/default_tools.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/tools/registry.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop/tools/spawn_agent.py +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop.egg-info/SOURCES.txt +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop.egg-info/dependency_links.txt +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop.egg-info/requires.txt +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/power_loop.egg-info/top_level.txt +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/pyproject.toml +0 -0
- {power_loop-0.8.0 → power_loop-0.9.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: power-loop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -55,6 +55,11 @@ agent runs model-authored or otherwise untrusted commands, run them in your own
|
|
|
55
55
|
(`runtime.exec_backend`); power-loop launches the persistent shell through your backend.
|
|
56
56
|
Keep secrets in your orchestrator — the loop does not scrub the tool environment for you.
|
|
57
57
|
|
|
58
|
+
Likewise there is **no built-in scheduler/timer**: a session only runs while a `send()` /
|
|
59
|
+
`resume()` call is in flight. "Wake this agent again in 10 minutes" is orchestrator state —
|
|
60
|
+
keep it in your own durable store and call `send()` when it fires (that survives restarts;
|
|
61
|
+
an in-process timer would not).
|
|
62
|
+
|
|
58
63
|
## Install
|
|
59
64
|
|
|
60
65
|
```bash
|
|
@@ -156,6 +161,38 @@ with runtime_env_context(RuntimeEnv(workspace_dir=tenant_workspace)):
|
|
|
156
161
|
|
|
157
162
|
See [`examples/23_per_send_overrides.py`](examples/23_per_send_overrides.py).
|
|
158
163
|
|
|
164
|
+
### Token usage accounting
|
|
165
|
+
|
|
166
|
+
Every `send()` returns the run's cumulative token usage — summed over all LLM
|
|
167
|
+
calls of that run (tool loops make several) — so cost accounting needs no event
|
|
168
|
+
plumbing:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
res = await loop.send("…", session_id=sid)
|
|
172
|
+
res.usage
|
|
173
|
+
# {"prompt_tokens": 1234, "completion_tokens": 56, "cache_read_tokens": 0,
|
|
174
|
+
# "reasoning_tokens": 0, "total_tokens": 1290, "calls": 2}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
For per-call, real-time metering subscribe to the `usage_updated` event (one
|
|
178
|
+
per LLM call, tagged with `session_id`). See
|
|
179
|
+
[`examples/25_token_usage.py`](examples/25_token_usage.py).
|
|
180
|
+
|
|
181
|
+
### Crash recovery: `heal_pending`
|
|
182
|
+
|
|
183
|
+
A run killed mid tool-call leaves the session with unresolved `tool_calls`;
|
|
184
|
+
the next `send()` raises `SessionPendingError` (the message protocol forbids
|
|
185
|
+
continuing). Orchestrators whose runs can legitimately die (human interrupts,
|
|
186
|
+
process restarts) can opt into self-healing:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
res = await loop.send("…", session_id=sid, heal_pending=True)
|
|
190
|
+
# stale tool_calls are aborted with synthetic <aborted> results, then the
|
|
191
|
+
# send proceeds. Default remains raise — healing discards in-flight work.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Or recover explicitly with `resume(sid)` / `abort_pending(sid)`.
|
|
195
|
+
|
|
159
196
|
## Public API
|
|
160
197
|
|
|
161
198
|
Stable imports are re-exported from `power_loop`:
|
|
@@ -18,6 +18,11 @@ agent runs model-authored or otherwise untrusted commands, run them in your own
|
|
|
18
18
|
(`runtime.exec_backend`); power-loop launches the persistent shell through your backend.
|
|
19
19
|
Keep secrets in your orchestrator — the loop does not scrub the tool environment for you.
|
|
20
20
|
|
|
21
|
+
Likewise there is **no built-in scheduler/timer**: a session only runs while a `send()` /
|
|
22
|
+
`resume()` call is in flight. "Wake this agent again in 10 minutes" is orchestrator state —
|
|
23
|
+
keep it in your own durable store and call `send()` when it fires (that survives restarts;
|
|
24
|
+
an in-process timer would not).
|
|
25
|
+
|
|
21
26
|
## Install
|
|
22
27
|
|
|
23
28
|
```bash
|
|
@@ -119,6 +124,38 @@ with runtime_env_context(RuntimeEnv(workspace_dir=tenant_workspace)):
|
|
|
119
124
|
|
|
120
125
|
See [`examples/23_per_send_overrides.py`](examples/23_per_send_overrides.py).
|
|
121
126
|
|
|
127
|
+
### Token usage accounting
|
|
128
|
+
|
|
129
|
+
Every `send()` returns the run's cumulative token usage — summed over all LLM
|
|
130
|
+
calls of that run (tool loops make several) — so cost accounting needs no event
|
|
131
|
+
plumbing:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
res = await loop.send("…", session_id=sid)
|
|
135
|
+
res.usage
|
|
136
|
+
# {"prompt_tokens": 1234, "completion_tokens": 56, "cache_read_tokens": 0,
|
|
137
|
+
# "reasoning_tokens": 0, "total_tokens": 1290, "calls": 2}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
For per-call, real-time metering subscribe to the `usage_updated` event (one
|
|
141
|
+
per LLM call, tagged with `session_id`). See
|
|
142
|
+
[`examples/25_token_usage.py`](examples/25_token_usage.py).
|
|
143
|
+
|
|
144
|
+
### Crash recovery: `heal_pending`
|
|
145
|
+
|
|
146
|
+
A run killed mid tool-call leaves the session with unresolved `tool_calls`;
|
|
147
|
+
the next `send()` raises `SessionPendingError` (the message protocol forbids
|
|
148
|
+
continuing). Orchestrators whose runs can legitimately die (human interrupts,
|
|
149
|
+
process restarts) can opt into self-healing:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
res = await loop.send("…", session_id=sid, heal_pending=True)
|
|
153
|
+
# stale tool_calls are aborted with synthetic <aborted> results, then the
|
|
154
|
+
# send proceeds. Default remains raise — healing discards in-flight work.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Or recover explicitly with `resume(sid)` / `abort_pending(sid)`.
|
|
158
|
+
|
|
122
159
|
## Public API
|
|
123
160
|
|
|
124
161
|
Stable imports are re-exported from `power_loop`:
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import asyncio
|
|
22
22
|
import json
|
|
23
|
+
import logging
|
|
23
24
|
from collections.abc import Sequence
|
|
24
25
|
from dataclasses import dataclass, field, replace
|
|
25
26
|
from typing import Any
|
|
@@ -56,6 +57,8 @@ from power_loop.runtime.session_store import (
|
|
|
56
57
|
from power_loop.runtime.skills import SkillLoader
|
|
57
58
|
from power_loop.tools.registry import ToolRegistry
|
|
58
59
|
|
|
60
|
+
logger = logging.getLogger("power_loop.stateful")
|
|
61
|
+
|
|
59
62
|
|
|
60
63
|
@dataclass
|
|
61
64
|
class StatefulResult:
|
|
@@ -67,6 +70,10 @@ class StatefulResult:
|
|
|
67
70
|
rounds: int = 0
|
|
68
71
|
pending_tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
|
69
72
|
pending_interactions: list[dict[str, Any]] = field(default_factory=list)
|
|
73
|
+
#: Cumulative token usage for this send (summed over every LLM call):
|
|
74
|
+
#: {prompt_tokens, completion_tokens, cache_read_tokens, reasoning_tokens,
|
|
75
|
+
#: total_tokens, calls}. Empty when the run never reached the LLM.
|
|
76
|
+
usage: dict[str, int] = field(default_factory=dict)
|
|
70
77
|
|
|
71
78
|
|
|
72
79
|
class StatefulAgentLoop:
|
|
@@ -134,6 +141,7 @@ class StatefulAgentLoop:
|
|
|
134
141
|
stop_event: CancellationLike = None,
|
|
135
142
|
tools: Sequence[str] | ToolRegistry | None = None,
|
|
136
143
|
system_prompt: str | None = None,
|
|
144
|
+
heal_pending: bool = False,
|
|
137
145
|
) -> StatefulResult:
|
|
138
146
|
"""Append one user input to the session and run the loop.
|
|
139
147
|
|
|
@@ -149,13 +157,24 @@ class StatefulAgentLoop:
|
|
|
149
157
|
(precedence: this arg > session system_prompt > config).
|
|
150
158
|
|
|
151
159
|
Raises :class:`SessionPendingError` if the session has unresolved
|
|
152
|
-
tool_calls; the caller must call
|
|
153
|
-
:meth:`abort_pending` first
|
|
160
|
+
tool_calls (a previous run died mid tool-call); the caller must call
|
|
161
|
+
:meth:`resume` or :meth:`abort_pending` first — or pass
|
|
162
|
+
``heal_pending=True`` to have ``send`` abort the stale tool_calls
|
|
163
|
+
itself and proceed (the right default for orchestrators whose runs
|
|
164
|
+
can be killed, e.g. by a human interrupt).
|
|
154
165
|
"""
|
|
155
166
|
sid = session_id
|
|
156
167
|
async with self._lock_for(sid):
|
|
157
168
|
self._ensure_session_or_raise(sid)
|
|
158
|
-
|
|
169
|
+
if heal_pending:
|
|
170
|
+
healed = self._heal_pending(sid)
|
|
171
|
+
if healed:
|
|
172
|
+
logger.warning(
|
|
173
|
+
"send(heal_pending=True): aborted %d stale tool_call(s) "
|
|
174
|
+
"in session %s before proceeding", healed, sid,
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
self._raise_if_pending(sid)
|
|
159
178
|
self._persist_user_input(sid, user_input)
|
|
160
179
|
return await self._run_loop(
|
|
161
180
|
sid, stop_event=stop_event, tools=tools, system_prompt=system_prompt
|
|
@@ -217,6 +236,7 @@ class StatefulAgentLoop:
|
|
|
217
236
|
stop_event: CancellationLike = None,
|
|
218
237
|
tools: Sequence[str] | ToolRegistry | None = None,
|
|
219
238
|
system_prompt: str | None = None,
|
|
239
|
+
heal_pending: bool = False,
|
|
220
240
|
) -> StatefulResult:
|
|
221
241
|
return asyncio.run(
|
|
222
242
|
self.send(
|
|
@@ -225,6 +245,7 @@ class StatefulAgentLoop:
|
|
|
225
245
|
stop_event=stop_event,
|
|
226
246
|
tools=tools,
|
|
227
247
|
system_prompt=system_prompt,
|
|
248
|
+
heal_pending=heal_pending,
|
|
228
249
|
)
|
|
229
250
|
)
|
|
230
251
|
|
|
@@ -301,6 +322,14 @@ class StatefulAgentLoop:
|
|
|
301
322
|
|
|
302
323
|
return await self._run_loop(session_id, stop_event=stop_event, sink=sink)
|
|
303
324
|
|
|
325
|
+
def _heal_pending(self, sid: str) -> int:
|
|
326
|
+
"""abort_pending without the session-existence re-check (callers in
|
|
327
|
+
send already hold the lock and have validated the session)."""
|
|
328
|
+
state = self.store.get_state(sid)
|
|
329
|
+
if state is None or not state.pending:
|
|
330
|
+
return 0
|
|
331
|
+
return self.abort_pending(sid, reason="auto-healed by send(heal_pending=True)")
|
|
332
|
+
|
|
304
333
|
def abort_pending(self, session_id: str, *, reason: str = "aborted") -> int:
|
|
305
334
|
"""Synthesize ``<aborted: reason>`` tool messages for every unresolved
|
|
306
335
|
tool_call, restoring message-protocol validity. Returns the number of
|
|
@@ -593,6 +622,7 @@ class StatefulAgentLoop:
|
|
|
593
622
|
rounds=result.rounds,
|
|
594
623
|
pending_tool_calls=result.pending_tool_calls,
|
|
595
624
|
pending_interactions=result.pending_interactions,
|
|
625
|
+
usage=result.usage,
|
|
596
626
|
)
|
|
597
627
|
|
|
598
628
|
def _prime_sink_from_pending(self, sid: str, sink: SQLiteSink) -> None:
|
|
@@ -66,3 +66,7 @@ class AgentLoopResult:
|
|
|
66
66
|
pending_tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
|
67
67
|
pending_interactions: list[dict[str, Any]] = field(default_factory=list)
|
|
68
68
|
messages: list[LoopMessage] = field(default_factory=list)
|
|
69
|
+
#: Cumulative token usage across every LLM call of this run:
|
|
70
|
+
#: {prompt_tokens, completion_tokens, cache_read_tokens, reasoning_tokens,
|
|
71
|
+
#: total_tokens, calls}. Empty dict when the run never reached the LLM.
|
|
72
|
+
usage: dict[str, int] = field(default_factory=dict)
|
|
@@ -52,7 +52,10 @@ class SessionPendingError(PowerLoopError):
|
|
|
52
52
|
)
|
|
53
53
|
super().__init__(
|
|
54
54
|
f"session {session_id} has {len(pending_tool_calls)} unresolved tool_calls"
|
|
55
|
-
f" from round (assistant_seq={assistant_seq}): {names}"
|
|
55
|
+
f" from round (assistant_seq={assistant_seq}): {names}."
|
|
56
|
+
" Recover with resume(sid) to finish them, abort_pending(sid) to"
|
|
57
|
+
" discard them, or call send(..., heal_pending=True) to auto-abort"
|
|
58
|
+
" and proceed"
|
|
56
59
|
)
|
|
57
60
|
|
|
58
61
|
|
|
@@ -94,7 +94,14 @@ class ContextManager:
|
|
|
94
94
|
recent_files: list[str] = field(default_factory=list)
|
|
95
95
|
_file_counter: int = 0
|
|
96
96
|
|
|
97
|
+
#: Usage of the **last** LLM call only (overwritten by every
|
|
98
|
+
#: :meth:`update_usage`). For whole-run totals use :attr:`usage_totals`.
|
|
97
99
|
token_usage: dict[str, Any] = field(default_factory=dict)
|
|
100
|
+
#: Cumulative usage across every LLM call of this context's lifetime
|
|
101
|
+
#: (one context = one ``send``/run): prompt_tokens / completion_tokens /
|
|
102
|
+
#: cache_read_tokens / reasoning_tokens / total_tokens / calls.
|
|
103
|
+
#: Surfaced on ``AgentLoopResult.usage`` and ``StatefulResult.usage``.
|
|
104
|
+
usage_totals: dict[str, int] = field(default_factory=dict)
|
|
98
105
|
#: 可选计数器,供测试/扩展使用;**不会**被 ``update_usage`` 自动递增
|
|
99
106
|
api_calls: int = 0
|
|
100
107
|
|
|
@@ -165,6 +172,11 @@ class ContextManager:
|
|
|
165
172
|
"reasoning": reasoning,
|
|
166
173
|
}
|
|
167
174
|
self.token_usage = usage_out
|
|
175
|
+
totals = self.usage_totals
|
|
176
|
+
totals["calls"] = totals.get("calls", 0) + 1
|
|
177
|
+
for key in ("prompt_tokens", "completion_tokens", "cache_read_tokens",
|
|
178
|
+
"reasoning_tokens", "total_tokens"):
|
|
179
|
+
totals[key] = totals.get(key, 0) + usage_out[key]
|
|
168
180
|
return usage_out
|
|
169
181
|
|
|
170
182
|
def microcompact(self, messages: list[dict[str, Any]]) -> None:
|
|
@@ -190,5 +190,10 @@ class SQLiteNoteMemory:
|
|
|
190
190
|
return []
|
|
191
191
|
return [{"role": "system", "name": "memory_notes", "content": text}]
|
|
192
192
|
|
|
193
|
-
async def remember(
|
|
193
|
+
async def remember(
|
|
194
|
+
self,
|
|
195
|
+
*,
|
|
196
|
+
snapshot: Any,
|
|
197
|
+
session_id: str | None = None,
|
|
198
|
+
) -> None:
|
|
194
199
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: power-loop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -55,6 +55,11 @@ agent runs model-authored or otherwise untrusted commands, run them in your own
|
|
|
55
55
|
(`runtime.exec_backend`); power-loop launches the persistent shell through your backend.
|
|
56
56
|
Keep secrets in your orchestrator — the loop does not scrub the tool environment for you.
|
|
57
57
|
|
|
58
|
+
Likewise there is **no built-in scheduler/timer**: a session only runs while a `send()` /
|
|
59
|
+
`resume()` call is in flight. "Wake this agent again in 10 minutes" is orchestrator state —
|
|
60
|
+
keep it in your own durable store and call `send()` when it fires (that survives restarts;
|
|
61
|
+
an in-process timer would not).
|
|
62
|
+
|
|
58
63
|
## Install
|
|
59
64
|
|
|
60
65
|
```bash
|
|
@@ -156,6 +161,38 @@ with runtime_env_context(RuntimeEnv(workspace_dir=tenant_workspace)):
|
|
|
156
161
|
|
|
157
162
|
See [`examples/23_per_send_overrides.py`](examples/23_per_send_overrides.py).
|
|
158
163
|
|
|
164
|
+
### Token usage accounting
|
|
165
|
+
|
|
166
|
+
Every `send()` returns the run's cumulative token usage — summed over all LLM
|
|
167
|
+
calls of that run (tool loops make several) — so cost accounting needs no event
|
|
168
|
+
plumbing:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
res = await loop.send("…", session_id=sid)
|
|
172
|
+
res.usage
|
|
173
|
+
# {"prompt_tokens": 1234, "completion_tokens": 56, "cache_read_tokens": 0,
|
|
174
|
+
# "reasoning_tokens": 0, "total_tokens": 1290, "calls": 2}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
For per-call, real-time metering subscribe to the `usage_updated` event (one
|
|
178
|
+
per LLM call, tagged with `session_id`). See
|
|
179
|
+
[`examples/25_token_usage.py`](examples/25_token_usage.py).
|
|
180
|
+
|
|
181
|
+
### Crash recovery: `heal_pending`
|
|
182
|
+
|
|
183
|
+
A run killed mid tool-call leaves the session with unresolved `tool_calls`;
|
|
184
|
+
the next `send()` raises `SessionPendingError` (the message protocol forbids
|
|
185
|
+
continuing). Orchestrators whose runs can legitimately die (human interrupts,
|
|
186
|
+
process restarts) can opt into self-healing:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
res = await loop.send("…", session_id=sid, heal_pending=True)
|
|
190
|
+
# stale tool_calls are aborted with synthetic <aborted> results, then the
|
|
191
|
+
# send proceeds. Default remains raise — healing discards in-flight work.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Or recover explicitly with `resume(sid)` / `abort_pending(sid)`.
|
|
195
|
+
|
|
159
196
|
## Public API
|
|
160
197
|
|
|
161
198
|
Stable imports are re-exported from `power_loop`:
|
|
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
|