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,573 @@
1
+ """Base agent abstract class.
2
+
3
+ All agents (ReactAgent, WorkflowAgent) inherit from BaseAgent and use
4
+ the same constructor signature:
5
+
6
+ def __init__(self, ctx: InvocationContext, config: AgentConfig | None = None)
7
+
8
+ This enables:
9
+ - Unified agent creation via AgentFactory
10
+ - WorkflowAgent as SubAgent of ReactAgent and vice versa
11
+ - Consistent service access through InvocationContext
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ from abc import ABC, abstractmethod
17
+ from dataclasses import dataclass, field
18
+ from enum import Enum
19
+ from typing import Any, AsyncIterator, Callable, ClassVar, Literal, TYPE_CHECKING
20
+
21
+
22
+ class ToolInjectionMode(Enum):
23
+ """How tools are provided to LLM.
24
+
25
+ FUNCTION_CALL: Use native LLM function calling (tools parameter)
26
+ PROMPT: Inject tool schemas as text in system prompt
27
+ """
28
+ FUNCTION_CALL = "function_call"
29
+ PROMPT = "prompt"
30
+
31
+ if TYPE_CHECKING:
32
+ from .context import InvocationContext
33
+ from .types.block import BlockEvent, BlockKind, Persistence
34
+ from .types.action import ActionEvent, ActionCollector
35
+ from .types.session import Session
36
+ from .event_bus import EventBus, Bus, Events
37
+ from ..backends.state import StateBackend
38
+ from ..middleware import MiddlewareChain
39
+
40
+
41
+ @dataclass
42
+ class AgentConfig:
43
+ """Agent configuration.
44
+
45
+ Used by both ReactAgent and WorkflowAgent.
46
+
47
+ Note: LLM parameters (temperature, max_tokens, timeout, retries) are
48
+ configured on LLMProvider, not here. Tool timeout is configured on
49
+ each tool's ToolConfig, not here.
50
+ """
51
+ name: str = "default"
52
+ max_steps: int = 50
53
+
54
+ # System prompt configuration
55
+ system_prompt: str | None = None
56
+
57
+ # Thinking configuration (for models that support extended thinking)
58
+ enable_thinking: bool = False # Whether to request thinking output from LLM
59
+ reasoning_effort: str | None = None # Reasoning effort: "low", "medium", "high", "max", "auto"
60
+ stream_thinking: bool = True # Whether to stream thinking in real-time
61
+
62
+ # Tool execution configuration
63
+ parallel_tool_execution: bool = True # Execute multiple tools in parallel
64
+
65
+ # Tool injection mode
66
+ tool_mode: ToolInjectionMode = ToolInjectionMode.FUNCTION_CALL
67
+
68
+ # Extra metadata for agent-specific config
69
+ metadata: dict[str, Any] = field(default_factory=dict)
70
+
71
+ def to_dict(self) -> dict[str, Any]:
72
+ return {
73
+ "name": self.name,
74
+ "max_steps": self.max_steps,
75
+ "system_prompt": self.system_prompt,
76
+ "enable_thinking": self.enable_thinking,
77
+ "reasoning_effort": self.reasoning_effort,
78
+ "stream_thinking": self.stream_thinking,
79
+ "parallel_tool_execution": self.parallel_tool_execution,
80
+ "tool_mode": self.tool_mode.value,
81
+ "metadata": self.metadata,
82
+ }
83
+
84
+
85
+ class BaseAgent(ABC):
86
+ """Abstract base class for all agents.
87
+
88
+ All agents use the same constructor signature:
89
+ __init__(self, ctx: InvocationContext, config: AgentConfig | None = None)
90
+
91
+ This enables unified agent creation and interoperability:
92
+ - ReactAgent can delegate to WorkflowAgent
93
+ - WorkflowAgent nodes can use ReactAgent
94
+ - AgentFactory creates any agent type uniformly
95
+
96
+ Class-level attributes (override in subclasses):
97
+ name: Agent unique identifier
98
+ description: Human-readable description
99
+ agent_type: "react" or "workflow"
100
+ sub_agents: List of agent classes that can be delegated to
101
+ user_input_block: Function returning BlockEvent for HITL input
102
+ history_inherit_mode: How to inherit history on switch
103
+ memory_merge_mode: How to merge memory when completing as SubAgent
104
+ """
105
+
106
+ # ========== Class-level config (override in subclasses) ==========
107
+ name: ClassVar[str] = "base" # Agent unique identifier
108
+ description: ClassVar[str] = "" # Agent description
109
+
110
+ # Agent type - use any string for custom types
111
+ # Built-in types: "react", "workflow"
112
+ # Custom types: "custom", "data_processor", "summarizer", etc.
113
+ agent_type: ClassVar[str] = "custom"
114
+
115
+ # SubAgent configuration - list of agent classes that can be delegated to
116
+ sub_agents: ClassVar[list[type["BaseAgent"]]] = []
117
+
118
+ # HITL input block generator (called when agent needs initial input)
119
+ user_input_block: ClassVar[Callable[[], "BlockEvent"] | None] = None
120
+
121
+ # History inherit mode when switched to ("full", "summary", "none")
122
+ history_inherit_mode: ClassVar[Literal["full", "summary", "none"]] = "summary"
123
+
124
+ # Memory merge mode when completing as SubAgent ("merge", "summarize", "discard")
125
+ memory_merge_mode: ClassVar[Literal["merge", "summarize", "discard"]] = "summarize"
126
+
127
+ # ========== Instance initialization ==========
128
+
129
+ def __init__(
130
+ self,
131
+ ctx: "InvocationContext",
132
+ config: AgentConfig | None = None,
133
+ ):
134
+ """Initialize agent with context and config.
135
+
136
+ Args:
137
+ ctx: InvocationContext containing all services (storage, bus, llm, tools, etc.)
138
+ config: Agent configuration (optional, uses defaults if not provided)
139
+ """
140
+ self._ctx = ctx
141
+ self.config = config or AgentConfig()
142
+
143
+ # Abort signal (delegates to ctx.abort_*)
144
+ self._abort = asyncio.Event()
145
+
146
+ # Current state
147
+ self._running = False
148
+
149
+ # Action collector for current run
150
+ self._action_collector: "ActionCollector | None" = None
151
+
152
+ # Runtime config overrides (set by run(), cleared after execution)
153
+ self._run_config: dict[str, Any] = {}
154
+
155
+ # ========== Properties for service access ==========
156
+
157
+ @property
158
+ def ctx(self) -> "InvocationContext":
159
+ """Get invocation context."""
160
+ return self._ctx
161
+
162
+ @property
163
+ def bus(self) -> "Bus":
164
+ """Get event bus from context."""
165
+ return self._ctx.bus
166
+
167
+ @property
168
+ def session(self) -> "Session":
169
+ """Get session from context."""
170
+ return self._ctx.session
171
+
172
+ @property
173
+ def middleware(self) -> "MiddlewareChain | None":
174
+ """Get middleware chain from context."""
175
+ return self._ctx.middleware
176
+
177
+ async def run(
178
+ self,
179
+ input: Any,
180
+ *,
181
+ # Runtime config overrides (takes precedence over AgentConfig)
182
+ enable_thinking: bool | None = None,
183
+ reasoning_effort: str | None = None,
184
+ stream_thinking: bool | None = None,
185
+ llm: "LLMProvider | None" = None,
186
+ # Internal
187
+ _force_own_queue: bool = False,
188
+ ) -> AsyncIterator["BlockEvent | ActionEvent"]:
189
+ """Main entry point for agent execution.
190
+
191
+ This is a unified wrapper that:
192
+ 1. Sets up emit queue via ContextVar (or reuses parent's if nested)
193
+ 2. Runs _execute() which uses ctx.emit() internally
194
+ 3. Yields BlockEvent and non-internal ActionEvent
195
+ 4. Collects ActionEvents for result aggregation
196
+
197
+ Subclasses should implement _execute(), not run().
198
+ Uses event-driven asyncio.wait for minimal latency streaming.
199
+
200
+ Args:
201
+ input: Input to process (usually PromptInput)
202
+ enable_thinking: Override config.enable_thinking for this run
203
+ reasoning_effort: Override config.reasoning_effort for this run
204
+ stream_thinking: Override config.stream_thinking for this run
205
+ llm: Override context LLM provider for this run
206
+ _force_own_queue: Internal flag to force creating own queue
207
+ (used by delegate tool to capture events)
208
+
209
+ Yields:
210
+ BlockEvent for UI streaming
211
+ ActionEvent (if internal=False) for external actions
212
+
213
+ Example:
214
+ # Use thinking for complex tasks
215
+ async for event in agent.run(
216
+ "Solve this problem...",
217
+ enable_thinking=True,
218
+ reasoning_effort="high",
219
+ ):
220
+ print(event)
221
+
222
+ # Switch LLM for specific runs
223
+ async for event in agent.run(
224
+ "Quick task",
225
+ llm=faster_llm_provider,
226
+ ):
227
+ print(event)
228
+ """
229
+ from .context import _emit_queue_var, _set_current_ctx, _reset_current_ctx
230
+ from .types.block import BlockEvent
231
+ from .types.action import ActionEvent, ActionCollector
232
+
233
+ # Store runtime overrides for this run
234
+ self._run_config = {
235
+ "enable_thinking": enable_thinking,
236
+ "reasoning_effort": reasoning_effort,
237
+ "stream_thinking": stream_thinking,
238
+ "llm": llm,
239
+ }
240
+
241
+ # Auto-detect parent queue (nested agent call)
242
+ # Skip if _force_own_queue is True (delegate tool needs to capture events)
243
+ if not _force_own_queue:
244
+ try:
245
+ parent_queue = _emit_queue_var.get()
246
+ # Has parent - passthrough mode, reuse parent queue
247
+ # Still set our ctx (child agent has its own ctx)
248
+ ctx_token = _set_current_ctx(self._ctx)
249
+ try:
250
+ await self._execute(input)
251
+ finally:
252
+ _reset_current_ctx(ctx_token)
253
+ return
254
+ except LookupError:
255
+ pass
256
+
257
+ # No parent - create own queue and yield events
258
+ queue: asyncio.Queue[BlockEvent | ActionEvent] = asyncio.Queue()
259
+ queue_token = _emit_queue_var.set(queue)
260
+ ctx_token = _set_current_ctx(self._ctx)
261
+
262
+ # Create action collector for this run
263
+ self._action_collector = ActionCollector()
264
+
265
+ def process_event(event: BlockEvent | ActionEvent):
266
+ """Process event: collect actions, decide if should yield."""
267
+ if isinstance(event, ActionEvent):
268
+ self._action_collector.collect(event)
269
+ # Only yield non-internal actions
270
+ return not event.internal
271
+ return True # Always yield BlockEvents
272
+
273
+ try:
274
+ # Start execution task
275
+ exec_task = asyncio.create_task(self._execute(input))
276
+ get_task: asyncio.Task | None = None
277
+
278
+ # Collect and yield events (streaming)
279
+ while True:
280
+ # First drain any pending items from queue (non-blocking)
281
+ while True:
282
+ try:
283
+ event = queue.get_nowait()
284
+ if process_event(event):
285
+ yield event
286
+ except asyncio.QueueEmpty:
287
+ break
288
+
289
+ # Exit if task is done and queue is empty
290
+ if exec_task.done() and queue.empty():
291
+ break
292
+
293
+ # Create get_task if needed
294
+ if get_task is None or get_task.done():
295
+ get_task = asyncio.create_task(queue.get())
296
+
297
+ # Wait for EITHER: queue item OR exec_task completion
298
+ # This is event-driven - no polling, no timeout delays
299
+ done, _ = await asyncio.wait(
300
+ {get_task, exec_task},
301
+ return_when=asyncio.FIRST_COMPLETED,
302
+ )
303
+
304
+ if get_task in done:
305
+ # Got an item from queue
306
+ try:
307
+ event = get_task.result()
308
+ if process_event(event):
309
+ yield event
310
+ get_task = None # Will create new one in next iteration
311
+ except asyncio.CancelledError:
312
+ pass
313
+ # If only exec_task completed, loop will drain queue and exit
314
+
315
+ # Cancel pending get_task if any
316
+ if get_task and not get_task.done():
317
+ get_task.cancel()
318
+ try:
319
+ await get_task
320
+ except asyncio.CancelledError:
321
+ pass
322
+
323
+ # Final drain after task completion
324
+ while not queue.empty():
325
+ try:
326
+ event = queue.get_nowait()
327
+ if process_event(event):
328
+ yield event
329
+ except asyncio.QueueEmpty:
330
+ break
331
+
332
+ # Check for exceptions in execution
333
+ await exec_task
334
+
335
+ finally:
336
+ _reset_current_ctx(ctx_token)
337
+ _emit_queue_var.reset(queue_token)
338
+ self._run_config = {} # Clear runtime overrides
339
+
340
+ @abstractmethod
341
+ async def _execute(self, input: Any) -> None:
342
+ """Execute agent logic. Subclasses implement this.
343
+
344
+ Use self.ctx.emit(block) to send streaming output.
345
+
346
+ Args:
347
+ input: Input to process (usually PromptInput)
348
+ """
349
+ pass
350
+
351
+ def cancel(self, abort_chain: bool = False) -> None:
352
+ """Cancel current execution.
353
+
354
+ Args:
355
+ abort_chain: If True, abort entire invocation chain (SubAgents too)
356
+ """
357
+ self._abort.set()
358
+ if self._ctx:
359
+ if abort_chain:
360
+ self._ctx.abort_chain.set()
361
+ else:
362
+ self._ctx.abort_self.set()
363
+
364
+ def reset(self) -> None:
365
+ """Reset agent state for new execution."""
366
+ self._abort.clear()
367
+ self._running = False
368
+
369
+ @property
370
+ def is_running(self) -> bool:
371
+ """Check if agent is currently running."""
372
+ return self._running
373
+
374
+ @property
375
+ def is_cancelled(self) -> bool:
376
+ """Check if agent has been cancelled."""
377
+ if self._ctx and self._ctx.is_aborted:
378
+ return True
379
+ return self._abort.is_set()
380
+
381
+ # ========== Emit Helper Methods (Syntax Sugar) ==========
382
+
383
+ async def emit(self, event: "BlockEvent | ActionEvent") -> None:
384
+ """Emit a BlockEvent or ActionEvent.
385
+
386
+ Convenience method that delegates to ctx.emit().
387
+
388
+ Args:
389
+ event: BlockEvent or ActionEvent to emit
390
+ """
391
+ await self._ctx.emit(event)
392
+
393
+ async def emit_text(
394
+ self,
395
+ content: str,
396
+ block_id: str | None = None,
397
+ delta: bool = False,
398
+ parent_id: str | None = None,
399
+ ) -> str:
400
+ """Emit a text block.
401
+
402
+ Args:
403
+ content: Text content
404
+ block_id: Optional block ID (auto-generated if not provided)
405
+ delta: If True, emit as DELTA (append), else APPLY (create/replace)
406
+ parent_id: Optional parent block ID for nesting
407
+
408
+ Returns:
409
+ The block_id used
410
+
411
+ Example:
412
+ # Create new text block
413
+ block_id = await self.emit_text("Hello")
414
+
415
+ # Stream append to existing block
416
+ await self.emit_text(" World", block_id=block_id, delta=True)
417
+ """
418
+ from .types.block import BlockEvent, BlockKind, BlockOp
419
+ from .types.session import generate_id
420
+
421
+ bid = block_id or generate_id("blk")
422
+ await self._ctx.emit(BlockEvent(
423
+ block_id=bid,
424
+ parent_id=parent_id,
425
+ kind=BlockKind.TEXT,
426
+ op=BlockOp.DELTA if delta else BlockOp.APPLY,
427
+ data={"content": content},
428
+ ))
429
+ return bid
430
+
431
+ async def emit_thinking(
432
+ self,
433
+ content: str,
434
+ block_id: str | None = None,
435
+ delta: bool = False,
436
+ ) -> str:
437
+ """Emit a thinking block (collapsible in UI).
438
+
439
+ Args:
440
+ content: Thinking content
441
+ block_id: Optional block ID
442
+ delta: If True, emit as DELTA
443
+
444
+ Returns:
445
+ The block_id used
446
+ """
447
+ from .types.block import BlockEvent, BlockKind, BlockOp
448
+ from .types.session import generate_id
449
+
450
+ bid = block_id or generate_id("blk")
451
+ await self._ctx.emit(BlockEvent(
452
+ block_id=bid,
453
+ kind=BlockKind.THINKING,
454
+ op=BlockOp.DELTA if delta else BlockOp.APPLY,
455
+ data={"content": content},
456
+ ))
457
+ return bid
458
+
459
+ async def emit_error(
460
+ self,
461
+ message: str,
462
+ code: str = "error",
463
+ recoverable: bool = True,
464
+ block_id: str | None = None,
465
+ ) -> str:
466
+ """Emit an error block.
467
+
468
+ Args:
469
+ message: Error message
470
+ code: Error code
471
+ recoverable: Whether the error is recoverable
472
+ block_id: Optional block ID
473
+
474
+ Returns:
475
+ The block_id used
476
+ """
477
+ from .types.block import BlockEvent, BlockKind, BlockOp
478
+ from .types.session import generate_id
479
+
480
+ bid = block_id or generate_id("blk")
481
+ await self._ctx.emit(BlockEvent(
482
+ block_id=bid,
483
+ kind=BlockKind.ERROR,
484
+ op=BlockOp.APPLY,
485
+ data={
486
+ "code": code,
487
+ "message": message,
488
+ "recoverable": recoverable,
489
+ },
490
+ ))
491
+ return bid
492
+
493
+ async def emit_artifact(
494
+ self,
495
+ artifact_id: str,
496
+ title: str | None = None,
497
+ summary: str | None = None,
498
+ block_id: str | None = None,
499
+ ) -> str:
500
+ """Emit an artifact reference block.
501
+
502
+ Args:
503
+ artifact_id: ID of the artifact to reference
504
+ title: Optional title for display
505
+ summary: Optional summary for display
506
+ block_id: Optional block ID
507
+
508
+ Returns:
509
+ The block_id used
510
+ """
511
+ from .types.block import BlockEvent, BlockKind, BlockOp
512
+ from .types.session import generate_id
513
+
514
+ bid = block_id or generate_id("blk")
515
+ await self._ctx.emit(BlockEvent(
516
+ block_id=bid,
517
+ kind=BlockKind.ARTIFACT,
518
+ op=BlockOp.APPLY,
519
+ data={
520
+ "artifact_id": artifact_id,
521
+ "title": title,
522
+ "summary": summary,
523
+ },
524
+ ))
525
+ return bid
526
+
527
+ @property
528
+ def action_collector(self) -> "ActionCollector | None":
529
+ """Get action collector from current run.
530
+
531
+ Use this after run() completes to access collected actions and results.
532
+
533
+ Example:
534
+ async for event in agent.run(input):
535
+ ...
536
+ result = agent.action_collector.get_merged_result()
537
+ """
538
+ return self._action_collector
539
+
540
+ async def pause(self) -> str:
541
+ """Pause execution and return invocation ID for later resume.
542
+
543
+ Returns:
544
+ Invocation ID for resuming
545
+
546
+ Raises:
547
+ NotImplementedError: If agent doesn't support pause
548
+ """
549
+ raise NotImplementedError("Agent does not support pause/resume")
550
+
551
+ async def resume(self, invocation_id: str) -> AsyncIterator["BlockEvent"]:
552
+ """Resume paused execution.
553
+
554
+ Args:
555
+ invocation_id: ID from pause()
556
+
557
+ Yields:
558
+ BlockEvent streaming events
559
+
560
+ Raises:
561
+ NotImplementedError: If agent doesn't support pause
562
+ """
563
+ raise NotImplementedError("Agent does not support pause/resume")
564
+
565
+ async def _check_abort(self) -> bool:
566
+ """Check if execution should be aborted.
567
+
568
+ Returns:
569
+ True if should abort
570
+ """
571
+ if self._ctx and self._ctx.is_aborted:
572
+ return True
573
+ return self._abort.is_set()