power-loop 0.2.0__py3-none-any.whl

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 (53) hide show
  1. llm_client/__init__.py +0 -0
  2. llm_client/capabilities.py +162 -0
  3. llm_client/interface.py +470 -0
  4. llm_client/llm_factory.py +981 -0
  5. llm_client/llm_tooling.py +645 -0
  6. llm_client/llm_utils.py +205 -0
  7. llm_client/multimodal.py +237 -0
  8. llm_client/qwen_image.py +576 -0
  9. llm_client/web_search.py +149 -0
  10. power_loop/__init__.py +326 -0
  11. power_loop/agent/__init__.py +6 -0
  12. power_loop/agent/sink.py +247 -0
  13. power_loop/agent/stateful_loop.py +363 -0
  14. power_loop/agent/system_prompt.py +396 -0
  15. power_loop/agent/types.py +41 -0
  16. power_loop/contracts/__init__.py +132 -0
  17. power_loop/contracts/errors.py +140 -0
  18. power_loop/contracts/event_payloads.py +278 -0
  19. power_loop/contracts/events.py +86 -0
  20. power_loop/contracts/handlers.py +45 -0
  21. power_loop/contracts/hook_contexts.py +265 -0
  22. power_loop/contracts/hooks.py +64 -0
  23. power_loop/contracts/messages.py +90 -0
  24. power_loop/contracts/protocols.py +48 -0
  25. power_loop/contracts/tools.py +56 -0
  26. power_loop/core/agent_context.py +94 -0
  27. power_loop/core/events.py +124 -0
  28. power_loop/core/hooks.py +122 -0
  29. power_loop/core/phase.py +217 -0
  30. power_loop/core/pipeline.py +880 -0
  31. power_loop/core/runner.py +60 -0
  32. power_loop/core/state.py +208 -0
  33. power_loop/runtime/budget.py +179 -0
  34. power_loop/runtime/cancellation.py +127 -0
  35. power_loop/runtime/compact.py +300 -0
  36. power_loop/runtime/env.py +103 -0
  37. power_loop/runtime/memory.py +107 -0
  38. power_loop/runtime/provider.py +176 -0
  39. power_loop/runtime/retry.py +182 -0
  40. power_loop/runtime/session_store.py +636 -0
  41. power_loop/runtime/skills.py +201 -0
  42. power_loop/runtime/spec.py +233 -0
  43. power_loop/runtime/structured.py +225 -0
  44. power_loop/tools/__init__.py +51 -0
  45. power_loop/tools/default_manifest.py +244 -0
  46. power_loop/tools/default_tools.py +766 -0
  47. power_loop/tools/registry.py +162 -0
  48. power_loop/tools/spawn_agent.py +173 -0
  49. power_loop-0.2.0.dist-info/METADATA +632 -0
  50. power_loop-0.2.0.dist-info/RECORD +53 -0
  51. power_loop-0.2.0.dist-info/WHEEL +5 -0
  52. power_loop-0.2.0.dist-info/licenses/LICENSE +21 -0
  53. power_loop-0.2.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,265 @@
1
+ """Typed hook context dataclasses — one per hook point.
2
+
3
+ Each dataclass declares the exact fields a hook handler receives and can
4
+ modify, replacing the untyped ``HookContext.values`` dict. Handlers
5
+ mutate the context in place and set ``directive`` when needed.
6
+
7
+ Usage::
8
+
9
+ def my_handler(ctx: ToolBeforeCtx) -> None:
10
+ if "rm -rf" in str(ctx.tool_args):
11
+ ctx.output = "[blocked by policy]"
12
+ ctx.directive = HookDirective.SKIP
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import threading
17
+ from dataclasses import dataclass, field
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from power_loop.contracts.hooks import HookDirective
21
+
22
+ if TYPE_CHECKING:
23
+ from llm_client.interface import LLMResponse
24
+ from power_loop.agent.types import LoopMessage
25
+
26
+
27
+ # ── Base ──
28
+
29
+
30
+ @dataclass
31
+ class BaseHookCtx:
32
+ """Common fields shared by all hook contexts."""
33
+
34
+ round_index: int = 0
35
+ directive: HookDirective = HookDirective.CONTINUE
36
+
37
+
38
+ # ── Session ──
39
+
40
+
41
+ @dataclass
42
+ class SessionStartCtx(BaseHookCtx):
43
+ """Context for :pyattr:`HookPoint.SESSION_START`.
44
+
45
+ Handler may modify ``messages``.
46
+ """
47
+
48
+ scope: str = "main"
49
+ messages: list[LoopMessage] = field(default_factory=list)
50
+ stop_event: threading.Event | None = None
51
+
52
+
53
+ @dataclass
54
+ class SessionEndCtx(BaseHookCtx):
55
+ """Context for :pyattr:`HookPoint.SESSION_END` (read-only)."""
56
+
57
+ scope: str = "main"
58
+ reason: str = ""
59
+ messages: list[LoopMessage] = field(default_factory=list)
60
+ final_text: str | None = None
61
+
62
+
63
+ # ── Round ──
64
+
65
+
66
+ @dataclass
67
+ class RoundStartCtx(BaseHookCtx):
68
+ """Context for :pyattr:`HookPoint.ROUND_START`.
69
+
70
+ Directives: BREAK (set ``reason``), SKIP.
71
+ Handler may modify ``messages``.
72
+ """
73
+
74
+ messages: list[LoopMessage] = field(default_factory=list)
75
+ stop_event: threading.Event | None = None
76
+ # Handler output
77
+ reason: str = ""
78
+
79
+
80
+ @dataclass
81
+ class RoundEndCtx(BaseHookCtx):
82
+ """Context for :pyattr:`HookPoint.ROUND_END` (read-only).
83
+
84
+ Both ``response_text`` and ``used_todo`` are always present.
85
+ """
86
+
87
+ messages: list[LoopMessage] = field(default_factory=list)
88
+ has_tools: bool = False
89
+ response_text: str = ""
90
+ used_todo: bool = False
91
+
92
+
93
+ # ── LLM ──
94
+
95
+
96
+ @dataclass
97
+ class LlmBeforeCtx(BaseHookCtx):
98
+ """Context for :pyattr:`HookPoint.LLM_BEFORE`.
99
+
100
+ Directives: SHORT_CIRCUIT (set ``output`` to an ``LLMResponse``), BREAK.
101
+ Handler may modify any input field.
102
+ """
103
+
104
+ messages: list[LoopMessage] = field(default_factory=list)
105
+ system_prompt: str = ""
106
+ tools: list[dict[str, Any]] | None = None
107
+ max_tokens: int = 8000
108
+ temperature: float = 0.0
109
+ # Handler output (for SHORT_CIRCUIT)
110
+ output: LLMResponse | None = None
111
+
112
+
113
+ @dataclass
114
+ class LlmAfterCtx(BaseHookCtx):
115
+ """Context for :pyattr:`HookPoint.LLM_AFTER`.
116
+
117
+ Directives: BREAK.
118
+ Handler may replace ``output``.
119
+ """
120
+
121
+ messages: list[LoopMessage] = field(default_factory=list)
122
+ output: LLMResponse | None = None
123
+
124
+
125
+ # ── Round decide ──
126
+
127
+
128
+ @dataclass
129
+ class RoundDecideCtx(BaseHookCtx):
130
+ """Context for :pyattr:`HookPoint.ROUND_DECIDE`.
131
+
132
+ Directives: SKIP (set ``output`` as skip message), BREAK.
133
+ """
134
+
135
+ messages: list[LoopMessage] = field(default_factory=list)
136
+ tool_calls: list[dict[str, Any]] = field(default_factory=list)
137
+ assistant_text: str = ""
138
+ # Handler output (for SKIP)
139
+ output: str = "[skipped by round_decide hook]"
140
+
141
+
142
+ # ── Tools batch ──
143
+
144
+
145
+ @dataclass
146
+ class ToolsBatchBeforeCtx(BaseHookCtx):
147
+ """Context for :pyattr:`HookPoint.TOOLS_BATCH_BEFORE`.
148
+
149
+ Directives: SKIP (set ``output`` as placeholder result for all tools).
150
+ """
151
+
152
+ messages: list[LoopMessage] = field(default_factory=list)
153
+ tool_calls: list[dict[str, Any]] = field(default_factory=list)
154
+ # Handler output (for SKIP)
155
+ output: str = "[skipped by batch hook]"
156
+
157
+
158
+ @dataclass
159
+ class ToolsBatchAfterCtx(BaseHookCtx):
160
+ """Context for :pyattr:`HookPoint.TOOLS_BATCH_AFTER` (read-only)."""
161
+
162
+ messages: list[LoopMessage] = field(default_factory=list)
163
+ used_todo: bool = False
164
+
165
+
166
+ # ── Individual tool ──
167
+
168
+
169
+ @dataclass
170
+ class ToolBeforeCtx(BaseHookCtx):
171
+ """Context for :pyattr:`HookPoint.TOOL_BEFORE`.
172
+
173
+ Directives: SKIP (set ``output``).
174
+ Handler may modify ``tool_name`` and ``tool_args``.
175
+ """
176
+
177
+ tool_call: dict[str, Any] = field(default_factory=dict)
178
+ tool_name: str = ""
179
+ tool_args: dict[str, Any] = field(default_factory=dict)
180
+ # Handler output (for SKIP)
181
+ output: str = "[skipped by hook]"
182
+
183
+
184
+ @dataclass
185
+ class ToolAfterCtx(BaseHookCtx):
186
+ """Context for :pyattr:`HookPoint.TOOL_AFTER`.
187
+
188
+ Directives: BREAK.
189
+ Handler may replace ``output`` and ``failed``.
190
+ """
191
+
192
+ tool_call: dict[str, Any] = field(default_factory=dict)
193
+ tool_name: str = ""
194
+ tool_args: dict[str, Any] = field(default_factory=dict)
195
+ output: str = ""
196
+ failed: bool = False
197
+
198
+
199
+ @dataclass
200
+ class ToolErrorCtx(BaseHookCtx):
201
+ """Context for :pyattr:`HookPoint.TOOL_ERROR`.
202
+
203
+ Directives: SKIP (use ``output`` as fallback), SHORT_CIRCUIT (retry).
204
+ """
205
+
206
+ tool_call: dict[str, Any] = field(default_factory=dict)
207
+ tool_name: str = ""
208
+ tool_args: dict[str, Any] = field(default_factory=dict)
209
+ error: Exception | None = None
210
+ error_message: str = ""
211
+ # Handler output (fallback for SKIP)
212
+ output: str = ""
213
+
214
+
215
+ # ── Compact ──
216
+
217
+
218
+ @dataclass
219
+ class CompactBeforeCtx(BaseHookCtx):
220
+ """Context for :pyattr:`HookPoint.COMPACT_BEFORE`.
221
+
222
+ Directives: SKIP (skip compaction this round).
223
+ """
224
+
225
+ messages: list[LoopMessage] = field(default_factory=list)
226
+
227
+
228
+ @dataclass
229
+ class CompactAfterCtx(BaseHookCtx):
230
+ """Context for :pyattr:`HookPoint.COMPACT_AFTER` (read-only)."""
231
+
232
+ messages: list[LoopMessage] = field(default_factory=list)
233
+ messages_before_count: int = 0
234
+ messages_after_count: int = 0
235
+
236
+
237
+ # ── Message ──
238
+
239
+
240
+ @dataclass
241
+ class MessageAppendCtx(BaseHookCtx):
242
+ """Context for :pyattr:`HookPoint.MESSAGE_APPEND`.
243
+
244
+ Handler may modify ``message``.
245
+ """
246
+
247
+ message: dict[str, Any] = field(default_factory=dict)
248
+ session_id: str | None = None
249
+
250
+
251
+ # ── Memory (M1.9) ──
252
+
253
+
254
+ @dataclass
255
+ class MemoryRecalledCtx(BaseHookCtx):
256
+ """Context for :pyattr:`HookPoint.MEMORY_RECALLED`.
257
+
258
+ Fired after :meth:`MemoryProvider.recall` returns, before injection.
259
+ Handler may mutate ``recalled`` (filter, redact, reorder) or set
260
+ ``directive=SKIP`` to drop everything and inject nothing.
261
+ """
262
+
263
+ recalled: list[dict[str, Any]] = field(default_factory=list)
264
+ session_id: str | None = None
265
+ budget_tokens: int = 1500
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+
8
+ class HookPoint(str, Enum):
9
+ SESSION_START = "session.start"
10
+ SESSION_END = "session.end"
11
+ ROUND_START = "round.start"
12
+ ROUND_END = "round.end"
13
+ LLM_BEFORE = "llm.before"
14
+ LLM_AFTER = "llm.after"
15
+ TOOLS_BATCH_BEFORE = "tools.batch.before"
16
+ TOOLS_BATCH_AFTER = "tools.batch.after"
17
+ TOOL_BEFORE = "tool.before"
18
+ TOOL_AFTER = "tool.after"
19
+ TOOL_ERROR = "tool.error"
20
+ ROUND_DECIDE = "round.decide"
21
+ COMPACT_BEFORE = "compact.before"
22
+ COMPACT_AFTER = "compact.after"
23
+ MESSAGE_APPEND = "message.append"
24
+ MEMORY_RECALLED = "memory.recalled"
25
+
26
+
27
+ class HookDirective(str, Enum):
28
+ """Control-flow directives that hooks can return to influence the agent loop.
29
+
30
+ Not every directive is valid at every hook point. The agent loop checks
31
+ the directive returned by ``hooks.run_async`` and acts accordingly.
32
+
33
+ Supported combinations:
34
+ ROUND_START -> BREAK (end loop), SKIP (skip this round)
35
+ LLM_BEFORE -> SHORT_CIRCUIT (use values["response"] instead of calling LLM)
36
+ LLM_AFTER -> BREAK (end loop, ignore tool calls)
37
+ ROUND_DECIDE -> SKIP (skip tool execution), BREAK (end loop)
38
+ TOOLS_BATCH_BEFORE -> SKIP (skip all tools this round)
39
+ TOOL_BEFORE -> SKIP (skip this tool, use values["tool_output"] as result)
40
+ TOOL_ERROR -> SKIP (swallow error, use values["tool_output"]),
41
+ SHORT_CIRCUIT (retry — re-invoke the tool)
42
+ TOOL_AFTER -> BREAK (stop executing remaining tools, proceed to next round)
43
+ COMPACT_BEFORE -> SKIP (skip compaction this round)
44
+ """
45
+
46
+ CONTINUE = "continue"
47
+ SKIP = "skip"
48
+ BREAK = "break"
49
+ SHORT_CIRCUIT = "short_circuit"
50
+
51
+
52
+ @dataclass
53
+ class HookContext:
54
+ """Mutable context passed through each hook chain."""
55
+
56
+ values: dict[str, Any] = field(default_factory=dict)
57
+
58
+
59
+ @dataclass
60
+ class HookResult:
61
+ """Return value from hook execution, carrying both context and control directive."""
62
+
63
+ context: HookContext = field(default_factory=HookContext)
64
+ directive: HookDirective = HookDirective.CONTINUE
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Literal
5
+
6
+ MessageRole = Literal["system", "user", "assistant", "tool"]
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ToolCall:
11
+ """Normalized tool call contract emitted by assistant messages."""
12
+
13
+ id: str
14
+ name: str
15
+ arguments: dict[str, Any] = field(default_factory=dict)
16
+
17
+ def to_openai_tool_call(self) -> dict[str, Any]:
18
+ import json
19
+
20
+ return {
21
+ "id": self.id,
22
+ "type": "function",
23
+ "function": {
24
+ "name": self.name,
25
+ "arguments": json.dumps(self.arguments, ensure_ascii=False),
26
+ },
27
+ }
28
+
29
+
30
+ @dataclass
31
+ class AgentMessage:
32
+ """Unified runtime message contract used inside AgentLoop."""
33
+
34
+ role: MessageRole
35
+ content: str
36
+ tool_calls: list[ToolCall] = field(default_factory=list)
37
+ tool_call_id: str | None = None
38
+ name: str | None = None
39
+ metadata: dict[str, Any] = field(default_factory=dict)
40
+
41
+ def to_openai_message(self) -> dict[str, Any]:
42
+ payload: dict[str, Any] = {
43
+ "role": self.role,
44
+ "content": self.content,
45
+ }
46
+ if self.role == "assistant" and self.tool_calls:
47
+ payload["tool_calls"] = [call.to_openai_tool_call() for call in self.tool_calls]
48
+ if self.role == "tool" and self.tool_call_id:
49
+ payload["tool_call_id"] = self.tool_call_id
50
+ if self.name:
51
+ payload["name"] = self.name
52
+ return payload
53
+
54
+
55
+ def SystemMessage(content: str, **metadata: Any) -> AgentMessage:
56
+ return AgentMessage(role="system", content=content, metadata=dict(metadata))
57
+
58
+
59
+ def UserMessage(content: str, **metadata: Any) -> AgentMessage:
60
+ return AgentMessage(role="user", content=content, metadata=dict(metadata))
61
+
62
+
63
+ def AssistantMessage(
64
+ content: str,
65
+ *,
66
+ tool_calls: list[ToolCall] | None = None,
67
+ **metadata: Any,
68
+ ) -> AgentMessage:
69
+ return AgentMessage(
70
+ role="assistant",
71
+ content=content,
72
+ tool_calls=list(tool_calls or []),
73
+ metadata=dict(metadata),
74
+ )
75
+
76
+
77
+ def ToolResultMessage(
78
+ content: str,
79
+ *,
80
+ tool_call_id: str,
81
+ name: str | None = None,
82
+ **metadata: Any,
83
+ ) -> AgentMessage:
84
+ return AgentMessage(
85
+ role="tool",
86
+ content=content,
87
+ tool_call_id=tool_call_id,
88
+ name=name,
89
+ metadata=dict(metadata),
90
+ )
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable
4
+ from typing import Any, Protocol
5
+
6
+ from power_loop.contracts.events import AgentEvent, AgentEventType
7
+ from power_loop.contracts.handlers import EventHandler, HookHandler
8
+ from power_loop.contracts.hook_contexts import BaseHookCtx
9
+ from power_loop.contracts.hooks import HookContext, HookPoint, HookResult
10
+
11
+
12
+ class EventBusProtocol(Protocol):
13
+ def subscribe(self, event_type: AgentEventType | None, handler: EventHandler, *, priority: int = 0) -> None:
14
+ ...
15
+
16
+ def unsubscribe(self, handler: EventHandler) -> None:
17
+ ...
18
+
19
+ def publish(self, event: AgentEvent) -> None:
20
+ ...
21
+
22
+ async def publish_async(self, event: AgentEvent) -> None:
23
+ ...
24
+
25
+
26
+ class HookManagerProtocol(Protocol):
27
+ def register(self, hook_point: HookPoint | str, handler: HookHandler, *, order: int = 0) -> None:
28
+ ...
29
+
30
+ def clear(self, hook_point: HookPoint | str | None = None) -> None:
31
+ ...
32
+
33
+ def run(self, hook_point: HookPoint | str, context: HookContext) -> HookResult:
34
+ ...
35
+
36
+ async def run_async(self, hook_point: HookPoint | str, context: HookContext) -> HookResult:
37
+ ...
38
+
39
+ def run_typed(self, hook_point: HookPoint | str, ctx: BaseHookCtx) -> None:
40
+ ...
41
+
42
+ async def run_typed_async(self, hook_point: HookPoint | str, ctx: BaseHookCtx) -> None:
43
+ ...
44
+
45
+
46
+ class ToolArgsValidator(Protocol):
47
+ def __call__(self, tool_name: str, args: dict[str, Any]) -> str | None | Awaitable[str | None]:
48
+ ...
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ToolDefinition:
10
+ name: str
11
+ description: str
12
+ input_schema: dict[str, Any] = field(default_factory=lambda: {"type": "object", "properties": {}})
13
+ required_params: tuple[str, ...] = ()
14
+
15
+ def to_openai_tool(self) -> dict[str, Any]:
16
+ return {
17
+ "type": "function",
18
+ "function": {
19
+ "name": self.name,
20
+ "description": self.description,
21
+ "parameters": dict(self.input_schema),
22
+ },
23
+ }
24
+
25
+
26
+ DEFAULT_REQUIRED_PARAMS: dict[str, tuple[str, ...]] = {
27
+ "write_file": ("path", "content"),
28
+ "read_file": ("path",),
29
+ "edit_file": ("path", "old_text", "new_text"),
30
+ "apply_patch": ("path", "patch"),
31
+ "bash": ("command",),
32
+ "glob": ("pattern",),
33
+ "grep": ("pattern",),
34
+ "load_skill": ("name",),
35
+ "todo": ("items",),
36
+ "background_run": ("command",),
37
+ "web_search": ("query",),
38
+ "generate_image": ("prompt",),
39
+ "edit_image": ("image_paths", "prompt"),
40
+ }
41
+
42
+
43
+ def validate_tool_args(tool_name: str, args: Mapping[str, Any]) -> str | None:
44
+ required = DEFAULT_REQUIRED_PARAMS.get(tool_name)
45
+ if not required:
46
+ return None
47
+ missing = [param for param in required if param not in args]
48
+ if not missing:
49
+ return None
50
+ req = ", ".join(required)
51
+ miss = ", ".join(missing)
52
+ return (
53
+ f"Error: missing required parameter(s): {miss}. "
54
+ f"{tool_name} requires: {req}. "
55
+ "Please provide all required parameters as a valid JSON object."
56
+ )
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from contextvars import ContextVar, Token
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from power_loop.agent.stateful_loop import StatefulAgentLoop
8
+ from power_loop.core.events import AgentEventBus
9
+ from power_loop.core.hooks import AgentHooks
10
+ from power_loop.core.state import ContextManager
11
+
12
+
13
+ _current_event_bus: ContextVar[AgentEventBus | None] = ContextVar("power_loop_event_bus", default=None)
14
+ _current_hooks: ContextVar[AgentHooks | None] = ContextVar("power_loop_hooks", default=None)
15
+ _current_ctx: ContextVar[ContextManager | None] = ContextVar("power_loop_ctx", default=None)
16
+ _current_session_id: ContextVar[str | None] = ContextVar("power_loop_session_id", default=None)
17
+ _current_loop: ContextVar[StatefulAgentLoop | None] = ContextVar(
18
+ "power_loop_current_loop", default=None
19
+ )
20
+
21
+
22
+ def get_event_bus() -> AgentEventBus:
23
+ bus = _current_event_bus.get()
24
+ # Local import to avoid import cycles
25
+ from power_loop.core.events import DEFAULT_EVENT_BUS
26
+
27
+ return DEFAULT_EVENT_BUS if bus is None else bus
28
+
29
+
30
+ def get_hooks() -> AgentHooks:
31
+ hooks = _current_hooks.get()
32
+ from power_loop.core.hooks import DEFAULT_HOOKS
33
+
34
+ return DEFAULT_HOOKS if hooks is None else hooks
35
+
36
+
37
+ def get_ctx() -> ContextManager:
38
+ ctx = _current_ctx.get()
39
+ if ctx is None:
40
+ from power_loop.core.state import ContextManager
41
+
42
+ ctx = ContextManager(role="main")
43
+ _current_ctx.set(ctx)
44
+ return ctx
45
+ return ctx
46
+
47
+
48
+ def get_session_id() -> str | None:
49
+ return _current_session_id.get()
50
+
51
+
52
+ def get_current_loop() -> StatefulAgentLoop | None:
53
+ return _current_loop.get()
54
+
55
+
56
+ def set_current_loop(loop: StatefulAgentLoop | None) -> Token:
57
+ return _current_loop.set(loop)
58
+
59
+
60
+ def reset_current_loop(token: Token) -> None:
61
+ _current_loop.reset(token)
62
+
63
+
64
+ def set_event_bus(bus: AgentEventBus | None) -> Token:
65
+ return _current_event_bus.set(bus)
66
+
67
+
68
+ def set_hooks(hooks: AgentHooks | None) -> Token:
69
+ return _current_hooks.set(hooks)
70
+
71
+
72
+ def set_ctx(ctx: ContextManager | None) -> Token:
73
+ return _current_ctx.set(ctx)
74
+
75
+
76
+ def set_session_id(session_id: str | None) -> Token:
77
+ return _current_session_id.set(session_id)
78
+
79
+
80
+ def reset_event_bus(token: Token) -> None:
81
+ _current_event_bus.reset(token)
82
+
83
+
84
+ def reset_hooks(token: Token) -> None:
85
+ _current_hooks.reset(token)
86
+
87
+
88
+ def reset_ctx(token: Token) -> None:
89
+ _current_ctx.reset(token)
90
+
91
+
92
+ def reset_session_id(token: Token) -> None:
93
+ _current_session_id.reset(token)
94
+