aury-agent 0.0.4__py3-none-any.whl → 0.0.5__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/agents/context_providers/message.py +8 -5
- aury/agents/core/base.py +11 -0
- aury/agents/core/factory.py +8 -0
- aury/agents/core/parallel.py +26 -4
- aury/agents/core/state.py +25 -0
- aury/agents/core/types/tool.py +1 -0
- aury/agents/hitl/ask_user.py +44 -0
- aury/agents/llm/adapter.py +55 -26
- aury/agents/llm/openai.py +5 -1
- aury/agents/memory/manager.py +33 -2
- aury/agents/messages/store.py +27 -1
- aury/agents/middleware/base.py +57 -0
- aury/agents/middleware/chain.py +81 -18
- aury/agents/react/agent.py +161 -1484
- aury/agents/react/context.py +309 -0
- aury/agents/react/factory.py +301 -0
- aury/agents/react/pause.py +241 -0
- aury/agents/react/persistence.py +182 -0
- aury/agents/react/step.py +680 -0
- aury/agents/react/tools.py +318 -0
- aury/agents/tool/builtin/bash.py +11 -0
- aury/agents/tool/builtin/delegate.py +38 -3
- aury/agents/tool/builtin/edit.py +16 -0
- aury/agents/tool/builtin/plan.py +19 -0
- aury/agents/tool/builtin/read.py +13 -0
- aury/agents/tool/builtin/thinking.py +10 -4
- aury/agents/tool/builtin/yield_result.py +9 -6
- aury/agents/tool/set.py +23 -0
- aury/agents/workflow/adapter.py +22 -3
- aury/agents/workflow/executor.py +51 -7
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/METADATA +1 -1
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/RECORD +34 -28
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/WHEEL +0 -0
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Pause and resume helpers for ReactAgent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import TYPE_CHECKING, AsyncIterator
|
|
7
|
+
|
|
8
|
+
from ..core.event_bus import Events
|
|
9
|
+
from ..core.types.block import BlockEvent, BlockKind, BlockOp
|
|
10
|
+
from ..core.types import (
|
|
11
|
+
Invocation,
|
|
12
|
+
InvocationState,
|
|
13
|
+
ToolInvocationState,
|
|
14
|
+
)
|
|
15
|
+
from ..llm import LLMMessage
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .agent import ReactAgent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def pause_agent(agent: "ReactAgent") -> str:
|
|
22
|
+
"""Pause execution and return invocation ID for later resume.
|
|
23
|
+
|
|
24
|
+
Saves current state to the invocation for later resumption.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
agent: ReactAgent instance
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Invocation ID for resuming
|
|
31
|
+
"""
|
|
32
|
+
if not agent._current_invocation:
|
|
33
|
+
raise RuntimeError("No active invocation to pause")
|
|
34
|
+
|
|
35
|
+
# Mark as paused
|
|
36
|
+
agent._paused = True
|
|
37
|
+
agent._current_invocation.mark_paused()
|
|
38
|
+
|
|
39
|
+
# Save state for resumption
|
|
40
|
+
agent._current_invocation.agent_state = {
|
|
41
|
+
"step": agent._current_step,
|
|
42
|
+
"message_history": [
|
|
43
|
+
{"role": m.role, "content": m.content} for m in agent._message_history
|
|
44
|
+
],
|
|
45
|
+
"text_buffer": agent._text_buffer,
|
|
46
|
+
}
|
|
47
|
+
agent._current_invocation.step_count = agent._current_step
|
|
48
|
+
|
|
49
|
+
# Save pending tool calls
|
|
50
|
+
agent._current_invocation.pending_tool_ids = [
|
|
51
|
+
inv.tool_call_id
|
|
52
|
+
for inv in agent._tool_invocations
|
|
53
|
+
if inv.state == ToolInvocationState.CALL
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Persist invocation
|
|
57
|
+
if agent.ctx.backends and agent.ctx.backends.invocation:
|
|
58
|
+
await agent.ctx.backends.invocation.update(
|
|
59
|
+
agent._current_invocation.id,
|
|
60
|
+
agent._current_invocation.to_dict(),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
await agent.bus.publish(
|
|
64
|
+
Events.INVOCATION_PAUSE,
|
|
65
|
+
{
|
|
66
|
+
"invocation_id": agent._current_invocation.id,
|
|
67
|
+
"step": agent._current_step,
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return agent._current_invocation.id
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def resume_agent_internal(agent: "ReactAgent", invocation_id: str) -> None:
|
|
75
|
+
"""Internal resume logic using emit.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
agent: ReactAgent instance
|
|
79
|
+
invocation_id: Invocation ID to resume
|
|
80
|
+
"""
|
|
81
|
+
from .step import execute_step
|
|
82
|
+
from .tools import process_tool_results
|
|
83
|
+
from .persistence import save_assistant_message, save_tool_messages
|
|
84
|
+
|
|
85
|
+
# Load invocation
|
|
86
|
+
if not agent.ctx.backends or not agent.ctx.backends.invocation:
|
|
87
|
+
raise ValueError("No invocation backend available")
|
|
88
|
+
inv_data = await agent.ctx.backends.invocation.get(invocation_id)
|
|
89
|
+
if not inv_data:
|
|
90
|
+
raise ValueError(f"Invocation not found: {invocation_id}")
|
|
91
|
+
|
|
92
|
+
invocation = Invocation.from_dict(inv_data)
|
|
93
|
+
|
|
94
|
+
if invocation.state != InvocationState.PAUSED:
|
|
95
|
+
raise ValueError(f"Invocation is not paused: {invocation.state}")
|
|
96
|
+
|
|
97
|
+
# Restore state
|
|
98
|
+
agent._current_invocation = invocation
|
|
99
|
+
agent._paused = False
|
|
100
|
+
agent._running = True
|
|
101
|
+
|
|
102
|
+
agent_state = invocation.agent_state or {}
|
|
103
|
+
agent._current_step = agent_state.get("step", 0)
|
|
104
|
+
agent._text_buffer = agent_state.get("text_buffer", "")
|
|
105
|
+
|
|
106
|
+
# Restore message history
|
|
107
|
+
agent._message_history = [
|
|
108
|
+
LLMMessage(role=m["role"], content=m["content"])
|
|
109
|
+
for m in agent_state.get("message_history", [])
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
# Mark as running
|
|
113
|
+
invocation.state = InvocationState.RUNNING
|
|
114
|
+
|
|
115
|
+
await agent.bus.publish(
|
|
116
|
+
Events.INVOCATION_RESUME,
|
|
117
|
+
{
|
|
118
|
+
"invocation_id": invocation_id,
|
|
119
|
+
"step": agent._current_step,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Continue execution loop
|
|
124
|
+
try:
|
|
125
|
+
finish_reason = None
|
|
126
|
+
|
|
127
|
+
while not await agent._check_abort() and not agent._paused:
|
|
128
|
+
agent._current_step += 1
|
|
129
|
+
|
|
130
|
+
if agent._current_step > agent.config.max_steps:
|
|
131
|
+
await agent.ctx.emit(BlockEvent(
|
|
132
|
+
kind=BlockKind.ERROR,
|
|
133
|
+
op=BlockOp.APPLY,
|
|
134
|
+
data={"message": f"Max steps ({agent.config.max_steps}) exceeded"},
|
|
135
|
+
))
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
finish_reason = await execute_step(agent)
|
|
139
|
+
|
|
140
|
+
# Save assistant message (real-time persistence)
|
|
141
|
+
await save_assistant_message(agent)
|
|
142
|
+
|
|
143
|
+
if finish_reason == "end_turn" and not agent._tool_invocations:
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
if agent._tool_invocations:
|
|
147
|
+
await process_tool_results(agent)
|
|
148
|
+
|
|
149
|
+
# Save tool messages (real-time persistence)
|
|
150
|
+
await save_tool_messages(agent)
|
|
151
|
+
|
|
152
|
+
agent._tool_invocations.clear()
|
|
153
|
+
|
|
154
|
+
if not agent._paused:
|
|
155
|
+
agent._current_invocation.state = InvocationState.COMPLETED
|
|
156
|
+
agent._current_invocation.finished_at = __import__("datetime").datetime.now()
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
agent._current_invocation.state = InvocationState.FAILED
|
|
160
|
+
await agent.ctx.emit(BlockEvent(
|
|
161
|
+
kind=BlockKind.ERROR,
|
|
162
|
+
op=BlockOp.APPLY,
|
|
163
|
+
data={"message": str(e)},
|
|
164
|
+
))
|
|
165
|
+
raise
|
|
166
|
+
|
|
167
|
+
finally:
|
|
168
|
+
agent._running = False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def resume_agent(agent: "ReactAgent", invocation_id: str) -> AsyncIterator[BlockEvent]:
|
|
172
|
+
"""Resume paused execution.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
agent: ReactAgent instance
|
|
176
|
+
invocation_id: ID from pause()
|
|
177
|
+
|
|
178
|
+
Yields:
|
|
179
|
+
BlockEvent streaming events
|
|
180
|
+
"""
|
|
181
|
+
from ..core.context import _emit_queue_var
|
|
182
|
+
|
|
183
|
+
queue: asyncio.Queue[BlockEvent] = asyncio.Queue()
|
|
184
|
+
token = _emit_queue_var.set(queue)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
exec_task = asyncio.create_task(resume_agent_internal(agent, invocation_id))
|
|
188
|
+
get_task: asyncio.Task | None = None
|
|
189
|
+
|
|
190
|
+
# Event-driven processing - no timeout delays
|
|
191
|
+
while True:
|
|
192
|
+
# First drain any pending items from queue (non-blocking)
|
|
193
|
+
while True:
|
|
194
|
+
try:
|
|
195
|
+
block = queue.get_nowait()
|
|
196
|
+
yield block
|
|
197
|
+
except asyncio.QueueEmpty:
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
# Exit if task is done and queue is empty
|
|
201
|
+
if exec_task.done() and queue.empty():
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
# Create get_task if needed
|
|
205
|
+
if get_task is None or get_task.done():
|
|
206
|
+
get_task = asyncio.create_task(queue.get())
|
|
207
|
+
|
|
208
|
+
# Wait for EITHER: queue item OR exec_task completion
|
|
209
|
+
done, _ = await asyncio.wait(
|
|
210
|
+
{get_task, exec_task},
|
|
211
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if get_task in done:
|
|
215
|
+
try:
|
|
216
|
+
block = get_task.result()
|
|
217
|
+
yield block
|
|
218
|
+
get_task = None
|
|
219
|
+
except asyncio.CancelledError:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
# Cancel pending get_task if any
|
|
223
|
+
if get_task and not get_task.done():
|
|
224
|
+
get_task.cancel()
|
|
225
|
+
try:
|
|
226
|
+
await get_task
|
|
227
|
+
except asyncio.CancelledError:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
# Final drain after task completion
|
|
231
|
+
while not queue.empty():
|
|
232
|
+
try:
|
|
233
|
+
block = queue.get_nowait()
|
|
234
|
+
yield block
|
|
235
|
+
except asyncio.QueueEmpty:
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
await exec_task
|
|
239
|
+
|
|
240
|
+
finally:
|
|
241
|
+
_emit_queue_var.reset(token)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""State persistence helpers for ReactAgent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .agent import ReactAgent
|
|
9
|
+
from ..core.types import PromptInput
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def save_messages_to_state(agent: "ReactAgent") -> None:
|
|
13
|
+
"""Save execution state for recovery.
|
|
14
|
+
|
|
15
|
+
This saves to state.execution namespace:
|
|
16
|
+
- step: current step number
|
|
17
|
+
- message_ids: references to raw messages (if using RawMessageMiddleware)
|
|
18
|
+
- For legacy/fallback: message_history as serialized data
|
|
19
|
+
|
|
20
|
+
Note: With RawMessageMiddleware, message_ids are automatically populated
|
|
21
|
+
by the middleware. This method saves additional execution state.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
agent: ReactAgent instance
|
|
25
|
+
"""
|
|
26
|
+
if not agent._state:
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
# Save step to execution namespace
|
|
30
|
+
agent._state.execution["step"] = agent._current_step
|
|
31
|
+
|
|
32
|
+
# Save invocation_id for recovery context
|
|
33
|
+
if agent._current_invocation:
|
|
34
|
+
agent._state.execution["invocation_id"] = agent._current_invocation.id
|
|
35
|
+
|
|
36
|
+
# Fallback: if message_ids not populated by middleware, save full history
|
|
37
|
+
# This ensures backward compatibility when RawMessageMiddleware is not used
|
|
38
|
+
if "message_ids" not in agent._state.execution:
|
|
39
|
+
messages_data = []
|
|
40
|
+
for msg in agent._message_history:
|
|
41
|
+
msg_dict = {"role": msg.role, "content": msg.content}
|
|
42
|
+
if hasattr(msg, "tool_call_id") and msg.tool_call_id:
|
|
43
|
+
msg_dict["tool_call_id"] = msg.tool_call_id
|
|
44
|
+
messages_data.append(msg_dict)
|
|
45
|
+
agent._state.execution["message_history"] = messages_data
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def clear_messages_from_state(agent: "ReactAgent") -> None:
|
|
49
|
+
"""Clear execution state after invocation completes.
|
|
50
|
+
|
|
51
|
+
Called when invocation completes normally. Historical messages
|
|
52
|
+
are already persisted (truncated) via MessageStore.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
agent: ReactAgent instance
|
|
56
|
+
"""
|
|
57
|
+
if not agent._state:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Clear execution namespace
|
|
61
|
+
agent._state.execution.clear()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def trigger_message_save(agent: "ReactAgent", message: dict) -> dict | None:
|
|
65
|
+
"""Trigger on_message_save hook via middleware.
|
|
66
|
+
|
|
67
|
+
Message persistence is handled by MessageBackendMiddleware.
|
|
68
|
+
Agent only triggers the hook, doesn't save directly.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
agent: ReactAgent instance
|
|
72
|
+
message: Message dict with role, content, etc.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Modified message or None if blocked
|
|
76
|
+
"""
|
|
77
|
+
# Check if message saving is disabled (e.g., for sub-agents with record_messages=False)
|
|
78
|
+
if getattr(agent, '_disable_message_save', False):
|
|
79
|
+
return message
|
|
80
|
+
|
|
81
|
+
if not agent.middleware:
|
|
82
|
+
return message
|
|
83
|
+
|
|
84
|
+
namespace = getattr(agent, '_message_namespace', None)
|
|
85
|
+
mw_context = {
|
|
86
|
+
"session_id": agent.session.id,
|
|
87
|
+
"agent_id": agent.name,
|
|
88
|
+
"namespace": namespace,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return await agent.middleware.process_message_save(message, mw_context)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def save_user_message(agent: "ReactAgent", input: "PromptInput") -> None:
|
|
95
|
+
"""Trigger save for user message.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
agent: ReactAgent instance
|
|
99
|
+
input: User prompt input
|
|
100
|
+
"""
|
|
101
|
+
# Build user content
|
|
102
|
+
content: str | list[dict] = input.text
|
|
103
|
+
if agent._agent_context and agent._agent_context.user_content:
|
|
104
|
+
content = agent._agent_context.user_content + "\n\n" + input.text
|
|
105
|
+
|
|
106
|
+
if input.attachments:
|
|
107
|
+
content_parts: list[dict] = [{"type": "text", "text": content}]
|
|
108
|
+
for attachment in input.attachments:
|
|
109
|
+
content_parts.append(attachment)
|
|
110
|
+
content = content_parts
|
|
111
|
+
|
|
112
|
+
# Build message and trigger hook
|
|
113
|
+
message = {
|
|
114
|
+
"role": "user",
|
|
115
|
+
"content": content,
|
|
116
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await trigger_message_save(agent, message)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def save_assistant_message(agent: "ReactAgent") -> None:
|
|
123
|
+
"""Trigger save for assistant message.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
agent: ReactAgent instance
|
|
127
|
+
"""
|
|
128
|
+
if not agent._text_buffer and not agent._tool_invocations and not agent._thinking_buffer:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
# Build assistant content
|
|
132
|
+
content: str | list[dict] = agent._text_buffer
|
|
133
|
+
if agent._tool_invocations or agent._thinking_buffer:
|
|
134
|
+
content_parts: list[dict] = []
|
|
135
|
+
# Claude requires thinking block first when thinking is enabled
|
|
136
|
+
if agent._thinking_buffer:
|
|
137
|
+
content_parts.append({"type": "thinking", "thinking": agent._thinking_buffer})
|
|
138
|
+
if agent._text_buffer:
|
|
139
|
+
content_parts.append({"type": "text", "text": agent._text_buffer})
|
|
140
|
+
for inv in agent._tool_invocations:
|
|
141
|
+
content_parts.append({
|
|
142
|
+
"type": "tool_use",
|
|
143
|
+
"id": inv.tool_call_id,
|
|
144
|
+
"name": inv.tool_name,
|
|
145
|
+
"input": inv.args,
|
|
146
|
+
})
|
|
147
|
+
content = content_parts
|
|
148
|
+
|
|
149
|
+
# Build message and trigger hook
|
|
150
|
+
message = {
|
|
151
|
+
"role": "assistant",
|
|
152
|
+
"content": content,
|
|
153
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await trigger_message_save(agent, message)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def save_tool_messages(agent: "ReactAgent") -> None:
|
|
160
|
+
"""Trigger save for tool result messages.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
agent: ReactAgent instance
|
|
164
|
+
"""
|
|
165
|
+
for inv in agent._tool_invocations:
|
|
166
|
+
if inv.result is not None:
|
|
167
|
+
# Build tool result message
|
|
168
|
+
content: list[dict] = [{
|
|
169
|
+
"type": "tool_result",
|
|
170
|
+
"tool_use_id": inv.tool_call_id,
|
|
171
|
+
"content": inv.result,
|
|
172
|
+
"is_error": inv.is_error,
|
|
173
|
+
}]
|
|
174
|
+
|
|
175
|
+
message = {
|
|
176
|
+
"role": "tool",
|
|
177
|
+
"content": content,
|
|
178
|
+
"tool_call_id": inv.tool_call_id,
|
|
179
|
+
"invocation_id": agent._current_invocation.id if agent._current_invocation else "",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await trigger_message_save(agent, message)
|