aury-agent 0.0.4__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 (149) hide show
  1. aury/__init__.py +2 -0
  2. aury/agents/__init__.py +55 -0
  3. aury/agents/a2a/__init__.py +168 -0
  4. aury/agents/backends/__init__.py +196 -0
  5. aury/agents/backends/artifact/__init__.py +9 -0
  6. aury/agents/backends/artifact/memory.py +130 -0
  7. aury/agents/backends/artifact/types.py +133 -0
  8. aury/agents/backends/code/__init__.py +65 -0
  9. aury/agents/backends/file/__init__.py +11 -0
  10. aury/agents/backends/file/local.py +66 -0
  11. aury/agents/backends/file/types.py +40 -0
  12. aury/agents/backends/invocation/__init__.py +8 -0
  13. aury/agents/backends/invocation/memory.py +81 -0
  14. aury/agents/backends/invocation/types.py +110 -0
  15. aury/agents/backends/memory/__init__.py +8 -0
  16. aury/agents/backends/memory/memory.py +179 -0
  17. aury/agents/backends/memory/types.py +136 -0
  18. aury/agents/backends/message/__init__.py +9 -0
  19. aury/agents/backends/message/memory.py +122 -0
  20. aury/agents/backends/message/types.py +124 -0
  21. aury/agents/backends/sandbox.py +275 -0
  22. aury/agents/backends/session/__init__.py +8 -0
  23. aury/agents/backends/session/memory.py +93 -0
  24. aury/agents/backends/session/types.py +124 -0
  25. aury/agents/backends/shell/__init__.py +11 -0
  26. aury/agents/backends/shell/local.py +110 -0
  27. aury/agents/backends/shell/types.py +55 -0
  28. aury/agents/backends/shell.py +209 -0
  29. aury/agents/backends/snapshot/__init__.py +19 -0
  30. aury/agents/backends/snapshot/git.py +95 -0
  31. aury/agents/backends/snapshot/hybrid.py +125 -0
  32. aury/agents/backends/snapshot/memory.py +86 -0
  33. aury/agents/backends/snapshot/types.py +59 -0
  34. aury/agents/backends/state/__init__.py +29 -0
  35. aury/agents/backends/state/composite.py +49 -0
  36. aury/agents/backends/state/file.py +57 -0
  37. aury/agents/backends/state/memory.py +52 -0
  38. aury/agents/backends/state/sqlite.py +262 -0
  39. aury/agents/backends/state/types.py +178 -0
  40. aury/agents/backends/subagent/__init__.py +165 -0
  41. aury/agents/cli/__init__.py +41 -0
  42. aury/agents/cli/chat.py +239 -0
  43. aury/agents/cli/config.py +236 -0
  44. aury/agents/cli/extensions.py +460 -0
  45. aury/agents/cli/main.py +189 -0
  46. aury/agents/cli/session.py +337 -0
  47. aury/agents/cli/workflow.py +276 -0
  48. aury/agents/context_providers/__init__.py +66 -0
  49. aury/agents/context_providers/artifact.py +299 -0
  50. aury/agents/context_providers/base.py +177 -0
  51. aury/agents/context_providers/memory.py +70 -0
  52. aury/agents/context_providers/message.py +130 -0
  53. aury/agents/context_providers/skill.py +50 -0
  54. aury/agents/context_providers/subagent.py +46 -0
  55. aury/agents/context_providers/tool.py +68 -0
  56. aury/agents/core/__init__.py +83 -0
  57. aury/agents/core/base.py +573 -0
  58. aury/agents/core/context.py +797 -0
  59. aury/agents/core/context_builder.py +303 -0
  60. aury/agents/core/event_bus/__init__.py +15 -0
  61. aury/agents/core/event_bus/bus.py +203 -0
  62. aury/agents/core/factory.py +169 -0
  63. aury/agents/core/isolator.py +97 -0
  64. aury/agents/core/logging.py +95 -0
  65. aury/agents/core/parallel.py +194 -0
  66. aury/agents/core/runner.py +139 -0
  67. aury/agents/core/services/__init__.py +5 -0
  68. aury/agents/core/services/file_session.py +144 -0
  69. aury/agents/core/services/message.py +53 -0
  70. aury/agents/core/services/session.py +53 -0
  71. aury/agents/core/signals.py +109 -0
  72. aury/agents/core/state.py +363 -0
  73. aury/agents/core/types/__init__.py +107 -0
  74. aury/agents/core/types/action.py +176 -0
  75. aury/agents/core/types/artifact.py +135 -0
  76. aury/agents/core/types/block.py +736 -0
  77. aury/agents/core/types/message.py +350 -0
  78. aury/agents/core/types/recall.py +144 -0
  79. aury/agents/core/types/session.py +257 -0
  80. aury/agents/core/types/subagent.py +154 -0
  81. aury/agents/core/types/tool.py +205 -0
  82. aury/agents/eval/__init__.py +331 -0
  83. aury/agents/hitl/__init__.py +57 -0
  84. aury/agents/hitl/ask_user.py +242 -0
  85. aury/agents/hitl/compaction.py +230 -0
  86. aury/agents/hitl/exceptions.py +87 -0
  87. aury/agents/hitl/permission.py +617 -0
  88. aury/agents/hitl/revert.py +216 -0
  89. aury/agents/llm/__init__.py +31 -0
  90. aury/agents/llm/adapter.py +367 -0
  91. aury/agents/llm/openai.py +294 -0
  92. aury/agents/llm/provider.py +476 -0
  93. aury/agents/mcp/__init__.py +153 -0
  94. aury/agents/memory/__init__.py +46 -0
  95. aury/agents/memory/compaction.py +394 -0
  96. aury/agents/memory/manager.py +465 -0
  97. aury/agents/memory/processor.py +177 -0
  98. aury/agents/memory/store.py +187 -0
  99. aury/agents/memory/types.py +137 -0
  100. aury/agents/messages/__init__.py +40 -0
  101. aury/agents/messages/config.py +47 -0
  102. aury/agents/messages/raw_store.py +224 -0
  103. aury/agents/messages/store.py +118 -0
  104. aury/agents/messages/types.py +88 -0
  105. aury/agents/middleware/__init__.py +31 -0
  106. aury/agents/middleware/base.py +341 -0
  107. aury/agents/middleware/chain.py +342 -0
  108. aury/agents/middleware/message.py +129 -0
  109. aury/agents/middleware/message_container.py +126 -0
  110. aury/agents/middleware/raw_message.py +153 -0
  111. aury/agents/middleware/truncation.py +139 -0
  112. aury/agents/middleware/types.py +81 -0
  113. aury/agents/plugin.py +162 -0
  114. aury/agents/react/__init__.py +4 -0
  115. aury/agents/react/agent.py +1923 -0
  116. aury/agents/sandbox/__init__.py +23 -0
  117. aury/agents/sandbox/local.py +239 -0
  118. aury/agents/sandbox/remote.py +200 -0
  119. aury/agents/sandbox/types.py +115 -0
  120. aury/agents/skill/__init__.py +16 -0
  121. aury/agents/skill/loader.py +180 -0
  122. aury/agents/skill/types.py +83 -0
  123. aury/agents/tool/__init__.py +39 -0
  124. aury/agents/tool/builtin/__init__.py +23 -0
  125. aury/agents/tool/builtin/ask_user.py +155 -0
  126. aury/agents/tool/builtin/bash.py +107 -0
  127. aury/agents/tool/builtin/delegate.py +726 -0
  128. aury/agents/tool/builtin/edit.py +121 -0
  129. aury/agents/tool/builtin/plan.py +277 -0
  130. aury/agents/tool/builtin/read.py +91 -0
  131. aury/agents/tool/builtin/thinking.py +111 -0
  132. aury/agents/tool/builtin/yield_result.py +130 -0
  133. aury/agents/tool/decorator.py +252 -0
  134. aury/agents/tool/set.py +204 -0
  135. aury/agents/usage/__init__.py +12 -0
  136. aury/agents/usage/tracker.py +236 -0
  137. aury/agents/workflow/__init__.py +85 -0
  138. aury/agents/workflow/adapter.py +268 -0
  139. aury/agents/workflow/dag.py +116 -0
  140. aury/agents/workflow/dsl.py +575 -0
  141. aury/agents/workflow/executor.py +659 -0
  142. aury/agents/workflow/expression.py +136 -0
  143. aury/agents/workflow/parser.py +182 -0
  144. aury/agents/workflow/state.py +145 -0
  145. aury/agents/workflow/types.py +86 -0
  146. aury_agent-0.0.4.dist-info/METADATA +90 -0
  147. aury_agent-0.0.4.dist-info/RECORD +149 -0
  148. aury_agent-0.0.4.dist-info/WHEEL +4 -0
  149. aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,797 @@
1
+ """Invocation context for agent execution.
2
+
3
+ InvocationContext is a runtime object that provides access to
4
+ the current execution context. It is NOT persisted - it is built
5
+ from the persisted Invocation when execution starts.
6
+
7
+ All services (llm, tools, middleware, etc.) are accessed through
8
+ this context, enabling unified agent construction.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ from contextvars import ContextVar
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, TYPE_CHECKING, AsyncIterator
16
+
17
+ from .logging import context_logger as logger
18
+ from .types.session import generate_id
19
+
20
+ # ContextVar for emit queue - shared across entire async call chain
21
+ _emit_queue_var: ContextVar[asyncio.Queue] = ContextVar('emit_queue')
22
+
23
+ # ContextVar for current parent_id - used by middleware to group blocks
24
+ # Stores tuple of (parent_id, apply_to_kinds) where apply_to_kinds is None (all) or set of kinds
25
+ _current_parent_id: ContextVar[tuple[str | None, set[str] | None]] = ContextVar(
26
+ 'current_parent_id', default=(None, None)
27
+ )
28
+
29
+ # ContextVar for current InvocationContext - flows through entire execution
30
+ _current_ctx: ContextVar["InvocationContext"] = ContextVar('current_invocation_context')
31
+
32
+
33
+ def get_current_ctx() -> "InvocationContext":
34
+ """Get current InvocationContext.
35
+
36
+ Use this to access ctx from anywhere within agent execution,
37
+ e.g., in Backend methods, tools, context providers.
38
+
39
+ Returns:
40
+ Current InvocationContext
41
+
42
+ Raises:
43
+ LookupError: If called outside of agent.run() context
44
+
45
+ Example:
46
+ from aury.agents.core.context import get_current_ctx
47
+
48
+ ctx = get_current_ctx()
49
+ session_id = ctx.session_id
50
+ agent_id = ctx.agent_id
51
+ """
52
+ return _current_ctx.get()
53
+
54
+
55
+ def get_current_ctx_or_none() -> "InvocationContext | None":
56
+ """Get current InvocationContext or None if not in agent context.
57
+
58
+ Safe version that doesn't raise exception.
59
+
60
+ Returns:
61
+ Current InvocationContext or None
62
+ """
63
+ try:
64
+ return _current_ctx.get()
65
+ except LookupError:
66
+ return None
67
+
68
+
69
+ def _set_current_ctx(ctx: "InvocationContext") -> object:
70
+ """Set current InvocationContext. Returns token for reset.
71
+
72
+ Internal use only - called by BaseAgent.run().
73
+ """
74
+ return _current_ctx.set(ctx)
75
+
76
+
77
+ def _reset_current_ctx(token: object) -> None:
78
+ """Reset InvocationContext using token from _set_current_ctx.
79
+
80
+ Internal use only - called by BaseAgent.run().
81
+ """
82
+ _current_ctx.reset(token)
83
+
84
+
85
+ def set_parent_id(
86
+ parent_id: str,
87
+ apply_to_kinds: set[str] | None = None,
88
+ ) -> object:
89
+ """Set current parent_id for block grouping.
90
+
91
+ Args:
92
+ parent_id: The parent block ID to set
93
+ apply_to_kinds: If provided, only blocks with these kinds will inherit
94
+ the parent_id. If None, all blocks inherit it.
95
+
96
+ Returns:
97
+ Token for reset. Use with middleware on_request/on_response.
98
+
99
+ Example:
100
+ # All blocks inherit parent_id
101
+ token = set_parent_id("blk_xxx")
102
+
103
+ # Only thinking and text blocks inherit parent_id
104
+ token = set_parent_id("blk_xxx", apply_to_kinds={"thinking", "text"})
105
+
106
+ # ... emit blocks
107
+ reset_parent_id(token)
108
+ """
109
+ return _current_parent_id.set((parent_id, apply_to_kinds))
110
+
111
+
112
+ def reset_parent_id(token: object) -> None:
113
+ """Reset parent_id to previous value using token from set_parent_id."""
114
+ _current_parent_id.reset(token)
115
+
116
+
117
+ def get_parent_id() -> str | None:
118
+ """Get current parent_id (for debugging/inspection)."""
119
+ parent_id, _ = _current_parent_id.get()
120
+ return parent_id
121
+
122
+
123
+ def resolve_parent_id(kind: str) -> str | None:
124
+ """Resolve parent_id for a given block kind.
125
+
126
+ Checks if the kind matches the apply_to_kinds filter.
127
+
128
+ Args:
129
+ kind: The block kind (e.g., "thinking", "text", "tool_use")
130
+
131
+ Returns:
132
+ parent_id if kind matches filter, None otherwise
133
+ """
134
+ parent_id, apply_to_kinds = _current_parent_id.get()
135
+ if parent_id is None:
136
+ return None
137
+ if apply_to_kinds is None:
138
+ return parent_id # No filter, apply to all
139
+ if kind in apply_to_kinds:
140
+ return parent_id
141
+ return None
142
+
143
+
144
+ async def emit(event: "BlockEvent | ActionEvent") -> None:
145
+ """Global emit function - emits to current run's queue via ContextVar.
146
+
147
+ Use this when you don't have access to InvocationContext,
148
+ e.g., in tool execute() methods.
149
+
150
+ For BlockEvent: automatically fills parent_id from ContextVar if not set.
151
+ ActionEvent does not have parent_id.
152
+
153
+ Args:
154
+ event: BlockEvent or ActionEvent to emit
155
+ """
156
+ try:
157
+ # Auto-fill parent_id from ContextVar if not explicitly set (BlockEvent only)
158
+ if hasattr(event, 'parent_id') and event.parent_id is None:
159
+ from .types.block import BlockKind
160
+ kind = event.kind.value if isinstance(event.kind, BlockKind) else event.kind
161
+ event.parent_id = resolve_parent_id(kind)
162
+
163
+ queue = _emit_queue_var.get()
164
+ await queue.put(event)
165
+ # Yield control to event loop to allow consumer to process the queue
166
+ # This ensures streaming output is truly streaming, not buffered
167
+ await asyncio.sleep(0)
168
+ except LookupError:
169
+ # Log warning if called outside of agent.run() context
170
+ pass
171
+
172
+ if TYPE_CHECKING:
173
+ from .types.session import Session, Invocation
174
+ from .types.message import Message
175
+ from .types.block import BlockEvent
176
+ from .types.action import ActionEvent
177
+ from .event_bus import EventBus, Events
178
+ from ..backends import Backends
179
+ from ..backends.state import StateBackend
180
+ from ..backends.snapshot import SnapshotBackend
181
+ from ..llm import LLMProvider
182
+ from ..tool import ToolSet, BaseTool, ToolResult
183
+ from ..middleware import MiddlewareChain, HookAction
184
+ from ..memory import MemoryManager
185
+ from ..usage import UsageTracker
186
+ from .state import State
187
+
188
+
189
+ @dataclass
190
+ class InvocationContext:
191
+ """Runtime context for an invocation.
192
+
193
+ This is the central object passed to all agents, providing access to:
194
+ - Core services: storage, bus, snapshot
195
+ - AI services: llm, tools
196
+ - Plugins: middleware
197
+ - Memory: memory manager
198
+ - Session info: session, invocation IDs
199
+
200
+ All agents (ReactAgent, WorkflowAgent) use the same constructor:
201
+ def __init__(self, ctx: InvocationContext, config: AgentConfig | None = None)
202
+
203
+ Attributes:
204
+ session: Current session object
205
+ invocation_id: Current invocation ID
206
+ agent_id: Current executing agent ID
207
+ backends: Backends container (new unified approach)
208
+ storage: State backend for persistence (legacy, prefer backends.state)
209
+ bus: Event bus for pub/sub
210
+ llm: LLM provider for AI calls
211
+ tools: Tool registry
212
+ middleware: Middleware chain
213
+ memory: Memory manager (optional)
214
+ snapshot: Snapshot backend for file tracking (optional)
215
+ parent_invocation_id: Parent invocation (for SubAgent)
216
+ mode: ROOT or DELEGATED
217
+ step: Current step number (mutable)
218
+ abort_self: Event to abort only this invocation
219
+ abort_chain: Event to abort entire invocation chain (shared)
220
+ config: Configuration options
221
+ metadata: Additional context data
222
+ """
223
+ # Core identifiers
224
+ session: "Session"
225
+ invocation_id: str
226
+ agent_id: str
227
+
228
+ # Backends container (unified backend access)
229
+ backends: "Backends | None" = None
230
+
231
+ # Core services (required)
232
+ bus: "EventBus | None" = None
233
+
234
+ # AI services (required for ReactAgent, optional for WorkflowAgent)
235
+ llm: "LLMProvider | None" = None
236
+ tools: "ToolSet | None" = None
237
+
238
+ # Plugin services
239
+ middleware: "MiddlewareChain | None" = None
240
+
241
+ # Memory
242
+ memory: "MemoryManager | None" = None
243
+
244
+ # Usage tracking
245
+ usage: "UsageTracker | None" = None
246
+
247
+ # Optional services
248
+ snapshot: "SnapshotBackend | None" = None
249
+
250
+ # State management (with checkpoint support)
251
+ state: "State | None" = None
252
+
253
+ # Current run input (set by agent.run(), accessible by Managers)
254
+ input: Any = None # PromptInput
255
+
256
+ # Current step's context (set before each LLM call, contains merged Manager outputs)
257
+ agent_context: Any = None # AgentContext
258
+
259
+ # Hierarchy
260
+ parent_invocation_id: str | None = None
261
+ mode: str = "root" # root or delegated
262
+ step: int = 0
263
+
264
+ # Tool execution context (set when executing a tool)
265
+ tool_call_id: str | None = None
266
+
267
+ # Abort signals
268
+ abort_self: asyncio.Event = field(default_factory=asyncio.Event)
269
+ abort_chain: asyncio.Event = field(default_factory=asyncio.Event)
270
+
271
+ # Config
272
+ config: dict[str, Any] = field(default_factory=dict)
273
+ metadata: dict[str, Any] = field(default_factory=dict)
274
+
275
+ # Depth tracking (for max depth enforcement)
276
+ _depth: int = 0
277
+
278
+ @property
279
+ def session_id(self) -> str:
280
+ """Get session ID (convenience property)."""
281
+ return self.session.id
282
+
283
+ @property
284
+ def depth(self) -> int:
285
+ """Get current invocation depth (0 = root)."""
286
+ return self._depth
287
+
288
+ @property
289
+ def is_aborted(self) -> bool:
290
+ """Check if this invocation should stop."""
291
+ return self.abort_self.is_set() or self.abort_chain.is_set()
292
+
293
+ @classmethod
294
+ def create(
295
+ cls,
296
+ agent_id: str = "agent",
297
+ session_id: str | None = None,
298
+ invocation_id: str | None = None,
299
+ backends: "Backends | None" = None,
300
+ bus: "EventBus | None" = None,
301
+ llm: "LLMProvider | None" = None,
302
+ tools: "ToolSet | None" = None,
303
+ middleware: "MiddlewareChain | None" = None,
304
+ memory: "MemoryManager | None" = None,
305
+ ) -> "InvocationContext":
306
+ """Create InvocationContext with auto-created defaults.
307
+
308
+ This is a convenience method for simple use cases.
309
+ Session and Bus are auto-created if not provided.
310
+
311
+ Args:
312
+ agent_id: Agent ID (default "agent")
313
+ session_id: Session ID (auto-generated if None)
314
+ invocation_id: Invocation ID (auto-generated if None)
315
+ backends: Backends container (recommended, auto-created if None)
316
+ bus: Event bus (auto-created if None)
317
+ llm: LLM provider (optional)
318
+ tools: Tool registry (optional)
319
+ middleware: Middleware chain (optional)
320
+ memory: Memory manager (optional)
321
+
322
+ Returns:
323
+ Configured InvocationContext
324
+ """
325
+ from .types.session import Session, generate_id
326
+ from .event_bus import EventBus
327
+ from ..backends import Backends
328
+
329
+ # Auto-create backends if not provided
330
+ if backends is None:
331
+ backends = Backends.create_default()
332
+
333
+ if bus is None:
334
+ bus = EventBus()
335
+
336
+ session = Session(
337
+ id=session_id or generate_id("sess"),
338
+ root_agent_id=agent_id,
339
+ )
340
+
341
+ return cls(
342
+ session=session,
343
+ invocation_id=invocation_id or generate_id("inv"),
344
+ agent_id=agent_id,
345
+ backends=backends,
346
+ bus=bus,
347
+ llm=llm,
348
+ tools=tools,
349
+ middleware=middleware,
350
+ memory=memory,
351
+ )
352
+
353
+ @classmethod
354
+ def from_invocation(
355
+ cls,
356
+ inv: "Invocation",
357
+ session: "Session",
358
+ backends: "Backends | None" = None,
359
+ bus: "EventBus | None" = None,
360
+ llm: "LLMProvider | None" = None,
361
+ tools: "ToolSet | None" = None,
362
+ middleware: "MiddlewareChain | None" = None,
363
+ memory: "MemoryManager | None" = None,
364
+ snapshot: "SnapshotBackend | None" = None,
365
+ ) -> "InvocationContext":
366
+ """Build context from persisted invocation.
367
+
368
+ Args:
369
+ inv: Persisted invocation
370
+ session: Session object
371
+ backends: Backends container (recommended, auto-created if None)
372
+ bus: Event bus
373
+ llm: LLM provider (required for ReactAgent)
374
+ tools: Tool registry (required for ReactAgent)
375
+ middleware: Middleware chain
376
+ memory: Memory manager
377
+ snapshot: Snapshot backend
378
+ """
379
+ from .event_bus import EventBus
380
+ from ..backends import Backends
381
+
382
+ # Auto-create backends if not provided
383
+ if backends is None:
384
+ backends = Backends.create_default()
385
+
386
+ if bus is None:
387
+ bus = EventBus()
388
+
389
+ return cls(
390
+ session=session,
391
+ invocation_id=inv.id,
392
+ agent_id=inv.agent_id,
393
+ backends=backends,
394
+ bus=bus,
395
+ llm=llm,
396
+ tools=tools,
397
+ middleware=middleware,
398
+ memory=memory,
399
+ snapshot=snapshot,
400
+ parent_invocation_id=inv.parent_invocation_id,
401
+ mode=inv.mode.value if hasattr(inv.mode, 'value') else str(inv.mode),
402
+ )
403
+
404
+ def create_child(
405
+ self,
406
+ agent_id: str,
407
+ mode: str = "delegated",
408
+ inherit_config: bool = True,
409
+ llm: "LLMProvider | None" = None,
410
+ tools: "ToolSet | None" = None,
411
+ middleware: "MiddlewareChain | None" = None,
412
+ parent_block_id: str | None = None,
413
+ ) -> "InvocationContext":
414
+ """Create child context for sub-agent execution.
415
+
416
+ Child context inherits services from parent by default.
417
+ LLM, tools, and middleware can be overridden for specialized sub-agents.
418
+
419
+ Args:
420
+ agent_id: Sub-agent ID
421
+ mode: Execution mode (delegated)
422
+ inherit_config: Whether to copy config
423
+ llm: Override LLM provider (None = inherit from parent)
424
+ tools: Override tool registry (None = inherit from parent)
425
+ middleware: Override middleware (None = inherit from parent)
426
+ parent_block_id: Parent block ID for nesting child's blocks.
427
+ If provided, sets _current_parent_id ContextVar
428
+ so all child's emitted blocks inherit this parent.
429
+
430
+ Returns:
431
+ New InvocationContext for child
432
+
433
+ Raises:
434
+ MaxDepthExceededError: If max depth exceeded
435
+ """
436
+ max_depth = self.config.get("max_sub_agent_depth", 5)
437
+ if self._depth >= max_depth:
438
+ logger.warning(
439
+ "Max sub-agent depth exceeded",
440
+ extra={"max_depth": max_depth, "agent_id": agent_id}
441
+ )
442
+ raise MaxDepthExceededError(f"Max sub-agent depth {max_depth} exceeded")
443
+
444
+ logger.debug(
445
+ "Creating child context",
446
+ extra={
447
+ "parent_inv": self.invocation_id,
448
+ "child_agent": agent_id,
449
+ "mode": mode,
450
+ "depth": self._depth + 1,
451
+ "parent_block_id": parent_block_id,
452
+ }
453
+ )
454
+
455
+ # If parent_block_id provided, set ContextVar so child's blocks inherit it
456
+ # This is done here (not in child ctx) because ContextVar is task-local
457
+ if parent_block_id:
458
+ set_parent_id(parent_block_id)
459
+
460
+ return InvocationContext(
461
+ session=self.session,
462
+ invocation_id=generate_id("inv"),
463
+ agent_id=agent_id,
464
+ backends=self.backends, # Inherit backends
465
+ bus=self.bus,
466
+ llm=llm if llm is not None else self.llm,
467
+ tools=tools if tools is not None else self.tools,
468
+ middleware=middleware if middleware is not None else self.middleware,
469
+ memory=self.memory,
470
+ usage=self.usage, # Share usage tracker
471
+ snapshot=self.snapshot,
472
+ parent_invocation_id=self.invocation_id,
473
+ mode=mode,
474
+ step=0,
475
+ abort_self=asyncio.Event(), # Child has own abort_self
476
+ abort_chain=self.abort_chain, # Shared abort_chain
477
+ config=self.config.copy() if inherit_config else {},
478
+ metadata={"parent_block_id": parent_block_id} if parent_block_id else {},
479
+ _depth=self._depth + 1,
480
+ )
481
+
482
+ def with_step(self, step: int) -> "InvocationContext":
483
+ """Create new context with updated step."""
484
+ return InvocationContext(
485
+ session=self.session,
486
+ invocation_id=self.invocation_id,
487
+ agent_id=self.agent_id,
488
+ backends=self.backends, # Inherit backends
489
+ bus=self.bus,
490
+ llm=self.llm,
491
+ tools=self.tools,
492
+ middleware=self.middleware,
493
+ memory=self.memory,
494
+ snapshot=self.snapshot,
495
+ parent_invocation_id=self.parent_invocation_id,
496
+ mode=self.mode,
497
+ step=step,
498
+ abort_self=self.abort_self,
499
+ abort_chain=self.abort_chain,
500
+ config=self.config,
501
+ metadata=self.metadata.copy(),
502
+ _depth=self._depth,
503
+ )
504
+
505
+ def fork(
506
+ self,
507
+ *,
508
+ tool_call_id: str | None = None,
509
+ step: int | None = None,
510
+ metadata: dict[str, Any] | None = None,
511
+ **kwargs: Any,
512
+ ) -> "InvocationContext":
513
+ """Create a forked context for parallel execution (e.g., tool calls).
514
+
515
+ Unlike create_child(), fork() creates a shallow copy with the same
516
+ invocation_id but different execution-specific fields. Use this for:
517
+ - Parallel tool execution (each tool gets its own tool_call_id)
518
+ - Step-specific context
519
+
520
+ The forked context shares:
521
+ - session, invocation_id, agent_id (same invocation)
522
+ - All services (storage, bus, llm, etc.)
523
+ - abort signals (shared)
524
+
525
+ The forked context has its own:
526
+ - tool_call_id (for tool-specific context)
527
+ - metadata (merged with parent)
528
+
529
+ Args:
530
+ tool_call_id: Tool call ID for this fork
531
+ step: Step number (None = inherit from parent)
532
+ metadata: Additional metadata (merged with parent)
533
+ **kwargs: Additional fields to override
534
+
535
+ Returns:
536
+ Forked InvocationContext
537
+
538
+ Example:
539
+ # Parallel tool execution
540
+ async def execute_tool(tool, args):
541
+ tool_ctx = ctx.fork(tool_call_id=tool.call_id)
542
+ token = _set_current_ctx(tool_ctx)
543
+ try:
544
+ return await tool.execute(args)
545
+ finally:
546
+ _reset_current_ctx(token)
547
+ """
548
+ from dataclasses import replace
549
+
550
+ # Build override dict
551
+ overrides: dict[str, Any] = {}
552
+
553
+ if tool_call_id is not None:
554
+ overrides["tool_call_id"] = tool_call_id
555
+ if step is not None:
556
+ overrides["step"] = step
557
+ if metadata:
558
+ merged = self.metadata.copy()
559
+ merged.update(metadata)
560
+ overrides["metadata"] = merged
561
+
562
+ overrides.update(kwargs)
563
+
564
+ return replace(self, **overrides)
565
+
566
+ # ========== Core Helper Methods ==========
567
+
568
+ async def emit(self, block: "BlockEvent") -> None:
569
+ """Emit a block event to the current run's queue.
570
+
571
+ This is the unified way to send streaming output from anywhere:
572
+ - ReactAgent LLM responses
573
+ - WorkflowAgent node outputs
574
+ - Tool outputs
575
+ - BlockHandle operations
576
+
577
+ The block's session_id, invocation_id, and parent_id are automatically filled.
578
+ Uses ContextVar to find the queue set by BaseAgent.run().
579
+ Parent_id respects the apply_to_kinds filter set via set_parent_id().
580
+
581
+ Args:
582
+ block: BlockEvent to emit
583
+ """
584
+ # Fill in IDs if not set
585
+ if not block.session_id:
586
+ block.session_id = self.session_id
587
+ if not block.invocation_id:
588
+ block.invocation_id = self.invocation_id
589
+ # Auto-fill parent_id from ContextVar if not explicitly set
590
+ # Uses resolve_parent_id to respect apply_to_kinds filter
591
+ if block.parent_id is None:
592
+ from .types.block import BlockKind
593
+ kind = block.kind.value if isinstance(block.kind, BlockKind) else block.kind
594
+ block.parent_id = resolve_parent_id(kind)
595
+
596
+ # Put into the current run's queue (via ContextVar)
597
+ try:
598
+ queue = _emit_queue_var.get()
599
+ await queue.put(block)
600
+ # Yield control to event loop to allow consumer to process the queue
601
+ # This ensures streaming output is truly streaming, not buffered
602
+ await asyncio.sleep(0)
603
+ except LookupError:
604
+ # Fallback: if no queue set, log warning (shouldn't happen in normal use)
605
+ logger.warning("emit() called outside of agent.run() context")
606
+
607
+ async def call_llm(
608
+ self,
609
+ messages: list["Message"],
610
+ llm: "LLMProvider | None" = None,
611
+ stream: bool = False,
612
+ **kwargs: Any,
613
+ ) -> Any | AsyncIterator[Any]:
614
+ """Call LLM with automatic middleware support.
615
+
616
+ Supports temporarily using a different LLM provider.
617
+ Automatically triggers on_request/on_response middleware hooks.
618
+
619
+ Args:
620
+ messages: Messages to send to LLM
621
+ llm: Override LLM provider (None = use ctx.llm)
622
+ stream: Whether to stream the response
623
+ **kwargs: Additional LLM parameters
624
+
625
+ Returns:
626
+ LLM response (or async iterator if streaming)
627
+
628
+ Raises:
629
+ ValueError: If no LLM available
630
+ """
631
+ provider = llm or self.llm
632
+ if provider is None:
633
+ raise ValueError("No LLM provider available")
634
+
635
+ # Build request
636
+ request = {
637
+ "messages": messages,
638
+ "stream": stream,
639
+ **kwargs,
640
+ }
641
+
642
+ # Build context for middleware
643
+ mw_context = {
644
+ "session_id": self.session_id,
645
+ "invocation_id": self.invocation_id,
646
+ "agent_id": self.agent_id,
647
+ "step": self.step,
648
+ }
649
+
650
+ # Process through middleware (on_request)
651
+ if self.middleware:
652
+ processed = await self.middleware.process_request(request, mw_context)
653
+ if processed is None:
654
+ logger.debug("Request blocked by middleware")
655
+ return None
656
+ request = processed
657
+
658
+ try:
659
+ # Call LLM
660
+ if stream:
661
+ return self._stream_llm_with_middleware(provider, request, mw_context)
662
+ else:
663
+ response = await provider.generate(
664
+ messages=request["messages"],
665
+ **{k: v for k, v in request.items() if k not in ("messages", "stream")}
666
+ )
667
+
668
+ # Process through middleware (on_response)
669
+ if self.middleware:
670
+ response_dict = {"response": response}
671
+ processed = await self.middleware.process_response(response_dict, mw_context)
672
+ if processed is None:
673
+ return None
674
+ response = processed.get("response", response)
675
+
676
+ return response
677
+
678
+ except Exception as e:
679
+ if self.middleware:
680
+ processed_error = await self.middleware.process_error(e, mw_context)
681
+ if processed_error is None:
682
+ return None
683
+ raise processed_error
684
+ raise
685
+
686
+ async def _stream_llm_with_middleware(
687
+ self,
688
+ provider: "LLMProvider",
689
+ request: dict[str, Any],
690
+ mw_context: dict[str, Any],
691
+ ) -> AsyncIterator[Any]:
692
+ """Stream LLM response with middleware processing."""
693
+ if self.middleware:
694
+ self.middleware.reset_stream_state()
695
+
696
+ try:
697
+ async for chunk in provider.stream(
698
+ messages=request["messages"],
699
+ **{k: v for k, v in request.items() if k not in ("messages", "stream")}
700
+ ):
701
+ if self.middleware:
702
+ chunk_dict = {"chunk": chunk}
703
+ processed = await self.middleware.process_stream_chunk(chunk_dict, mw_context)
704
+ if processed is None:
705
+ continue
706
+ chunk = processed.get("chunk", chunk)
707
+ yield chunk
708
+
709
+ except Exception as e:
710
+ if self.middleware:
711
+ processed_error = await self.middleware.process_error(e, mw_context)
712
+ if processed_error is None:
713
+ return
714
+ raise processed_error
715
+ raise
716
+
717
+ async def execute_tool(
718
+ self,
719
+ tool: "BaseTool",
720
+ arguments: dict[str, Any],
721
+ ) -> "ToolResult":
722
+ """Execute a tool with automatic middleware support.
723
+
724
+ Allows manual tool execution with custom arguments.
725
+ Automatically triggers on_tool_call/on_tool_end middleware hooks.
726
+
727
+ Args:
728
+ tool: The tool to execute
729
+ arguments: Tool arguments (manual input)
730
+
731
+ Returns:
732
+ Tool execution result
733
+ """
734
+ from ..middleware import HookAction
735
+
736
+ # Build context for middleware
737
+ mw_context = {
738
+ "session_id": self.session_id,
739
+ "invocation_id": self.invocation_id,
740
+ "agent_id": self.agent_id,
741
+ "step": self.step,
742
+ }
743
+
744
+ current_args = arguments
745
+
746
+ # Process through middleware (on_tool_call)
747
+ if self.middleware:
748
+ result = await self.middleware.process_tool_call(tool, current_args, mw_context)
749
+ if result.action == HookAction.SKIP:
750
+ logger.debug(f"Tool {tool.name} skipped by middleware")
751
+ from ..tool import ToolResult
752
+ return ToolResult(
753
+ output=result.message or "Skipped by middleware",
754
+ is_error=False,
755
+ )
756
+ elif result.action == HookAction.RETRY and result.modified_data:
757
+ current_args = result.modified_data
758
+ elif result.action == HookAction.STOP:
759
+ logger.debug(f"Tool {tool.name} stopped by middleware")
760
+ from ..tool import ToolResult
761
+ return ToolResult(
762
+ output=result.message or "Stopped by middleware",
763
+ is_error=True,
764
+ )
765
+
766
+ # Execute tool
767
+ tool_result = await tool.execute(**current_args)
768
+
769
+ # Process through middleware (on_tool_end)
770
+ if self.middleware:
771
+ result = await self.middleware.process_tool_end(tool, tool_result, mw_context)
772
+ if result.action == HookAction.RETRY and result.modified_data:
773
+ # Re-execute with modified args
774
+ tool_result = await tool.execute(**result.modified_data)
775
+
776
+ return tool_result
777
+
778
+
779
+ class MaxDepthExceededError(Exception):
780
+ """Raised when sub-agent nesting exceeds max depth."""
781
+ pass
782
+
783
+
784
+ __all__ = [
785
+ "InvocationContext",
786
+ "MaxDepthExceededError",
787
+ # Current context access
788
+ "get_current_ctx",
789
+ "get_current_ctx_or_none",
790
+ # Emit function
791
+ "emit",
792
+ # Parent ID management for block grouping
793
+ "set_parent_id",
794
+ "reset_parent_id",
795
+ "get_parent_id",
796
+ "resolve_parent_id",
797
+ ]