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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- 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
|
+
]
|