voxagent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voxagent/__init__.py +143 -0
- voxagent/_version.py +5 -0
- voxagent/agent/__init__.py +32 -0
- voxagent/agent/abort.py +178 -0
- voxagent/agent/core.py +902 -0
- voxagent/code/__init__.py +9 -0
- voxagent/mcp/__init__.py +16 -0
- voxagent/mcp/manager.py +188 -0
- voxagent/mcp/tool.py +152 -0
- voxagent/providers/__init__.py +110 -0
- voxagent/providers/anthropic.py +498 -0
- voxagent/providers/augment.py +293 -0
- voxagent/providers/auth.py +116 -0
- voxagent/providers/base.py +268 -0
- voxagent/providers/chatgpt.py +415 -0
- voxagent/providers/claudecode.py +162 -0
- voxagent/providers/cli_base.py +265 -0
- voxagent/providers/codex.py +183 -0
- voxagent/providers/failover.py +90 -0
- voxagent/providers/google.py +532 -0
- voxagent/providers/groq.py +96 -0
- voxagent/providers/ollama.py +425 -0
- voxagent/providers/openai.py +435 -0
- voxagent/providers/registry.py +175 -0
- voxagent/py.typed +1 -0
- voxagent/security/__init__.py +14 -0
- voxagent/security/events.py +75 -0
- voxagent/security/filter.py +169 -0
- voxagent/security/registry.py +87 -0
- voxagent/session/__init__.py +39 -0
- voxagent/session/compaction.py +237 -0
- voxagent/session/lock.py +103 -0
- voxagent/session/model.py +109 -0
- voxagent/session/storage.py +184 -0
- voxagent/streaming/__init__.py +52 -0
- voxagent/streaming/emitter.py +286 -0
- voxagent/streaming/events.py +255 -0
- voxagent/subagent/__init__.py +20 -0
- voxagent/subagent/context.py +124 -0
- voxagent/subagent/definition.py +172 -0
- voxagent/tools/__init__.py +32 -0
- voxagent/tools/context.py +50 -0
- voxagent/tools/decorator.py +175 -0
- voxagent/tools/definition.py +131 -0
- voxagent/tools/executor.py +109 -0
- voxagent/tools/policy.py +89 -0
- voxagent/tools/registry.py +89 -0
- voxagent/types/__init__.py +46 -0
- voxagent/types/messages.py +134 -0
- voxagent/types/run.py +176 -0
- voxagent-0.1.0.dist-info/METADATA +186 -0
- voxagent-0.1.0.dist-info/RECORD +53 -0
- voxagent-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Stream event data types.
|
|
2
|
+
|
|
3
|
+
This module defines typed event payloads for the streaming event system.
|
|
4
|
+
Each event type is a Pydantic model that can be serialized/deserialized.
|
|
5
|
+
|
|
6
|
+
Event Categories:
|
|
7
|
+
- Lifecycle Events: RunStartEvent, RunEndEvent, RunErrorEvent
|
|
8
|
+
- Inference Events: AssistantStartEvent, TextDeltaEvent, AssistantEndEvent
|
|
9
|
+
- Tool Events: ToolStartEvent, ToolOutputEvent, ToolEndEvent
|
|
10
|
+
- Context Events: CompactionStartEvent, CompactionEndEvent
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
from voxagent.types import Message, ToolCall, ToolResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _utc_now() -> datetime:
|
|
22
|
+
"""Return current UTC time."""
|
|
23
|
+
return datetime.now(timezone.utc)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Base Event
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseEvent(BaseModel):
|
|
32
|
+
"""Base class for all stream events.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
run_id: Unique identifier for the current agent run.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
run_id: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# Lifecycle Events
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RunStartEvent(BaseEvent):
|
|
47
|
+
"""Event emitted when an agent run starts.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
run_id: Unique identifier for the run.
|
|
51
|
+
session_key: Session key for the run.
|
|
52
|
+
timestamp: When the run started.
|
|
53
|
+
event_type: Discriminator field for type narrowing.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
session_key: str
|
|
57
|
+
timestamp: datetime = Field(default_factory=_utc_now)
|
|
58
|
+
event_type: Literal["run_start"] = "run_start"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RunEndEvent(BaseEvent):
|
|
62
|
+
"""Event emitted when an agent run ends.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
run_id: Unique identifier for the run.
|
|
66
|
+
messages: Final list of messages from the run.
|
|
67
|
+
aborted: Whether the run was aborted.
|
|
68
|
+
timed_out: Whether the run timed out.
|
|
69
|
+
timestamp: When the run ended.
|
|
70
|
+
event_type: Discriminator field for type narrowing.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
messages: list[Message]
|
|
74
|
+
aborted: bool = False
|
|
75
|
+
timed_out: bool = False
|
|
76
|
+
timestamp: datetime = Field(default_factory=_utc_now)
|
|
77
|
+
event_type: Literal["run_end"] = "run_end"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class RunErrorEvent(BaseEvent):
|
|
81
|
+
"""Event emitted when an agent run encounters an error.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
run_id: Unique identifier for the run.
|
|
85
|
+
error: Error message.
|
|
86
|
+
timestamp: When the error occurred.
|
|
87
|
+
event_type: Discriminator field for type narrowing.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
error: str
|
|
91
|
+
timestamp: datetime = Field(default_factory=_utc_now)
|
|
92
|
+
event_type: Literal["run_error"] = "run_error"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# =============================================================================
|
|
96
|
+
# Inference Events
|
|
97
|
+
# =============================================================================
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AssistantStartEvent(BaseEvent):
|
|
101
|
+
"""Event emitted when the assistant starts generating a response.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
run_id: Unique identifier for the run.
|
|
105
|
+
event_type: Discriminator field for type narrowing.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
event_type: Literal["assistant_start"] = "assistant_start"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TextDeltaEvent(BaseEvent):
|
|
112
|
+
"""Event emitted for each text chunk from the assistant.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
run_id: Unique identifier for the run.
|
|
116
|
+
delta: The text chunk.
|
|
117
|
+
event_type: Discriminator field for type narrowing.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
delta: str
|
|
121
|
+
event_type: Literal["text_delta"] = "text_delta"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AssistantEndEvent(BaseEvent):
|
|
125
|
+
"""Event emitted when the assistant finishes generating a response.
|
|
126
|
+
|
|
127
|
+
Attributes:
|
|
128
|
+
run_id: Unique identifier for the run.
|
|
129
|
+
message: The complete assistant message.
|
|
130
|
+
event_type: Discriminator field for type narrowing.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
message: Message
|
|
134
|
+
event_type: Literal["assistant_end"] = "assistant_end"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# =============================================================================
|
|
138
|
+
# Tool Events
|
|
139
|
+
# =============================================================================
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ToolStartEvent(BaseEvent):
|
|
143
|
+
"""Event emitted when a tool execution starts.
|
|
144
|
+
|
|
145
|
+
Attributes:
|
|
146
|
+
run_id: Unique identifier for the run.
|
|
147
|
+
tool_call: The tool call being executed.
|
|
148
|
+
event_type: Discriminator field for type narrowing.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
tool_call: ToolCall
|
|
152
|
+
event_type: Literal["tool_start"] = "tool_start"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ToolOutputEvent(BaseEvent):
|
|
156
|
+
"""Event emitted for streaming tool output.
|
|
157
|
+
|
|
158
|
+
Attributes:
|
|
159
|
+
run_id: Unique identifier for the run.
|
|
160
|
+
tool_call_id: ID of the tool call.
|
|
161
|
+
delta: Output chunk from the tool.
|
|
162
|
+
event_type: Discriminator field for type narrowing.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
tool_call_id: str
|
|
166
|
+
delta: str
|
|
167
|
+
event_type: Literal["tool_output"] = "tool_output"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ToolEndEvent(BaseEvent):
|
|
171
|
+
"""Event emitted when a tool execution ends.
|
|
172
|
+
|
|
173
|
+
Attributes:
|
|
174
|
+
run_id: Unique identifier for the run.
|
|
175
|
+
tool_call_id: ID of the tool call.
|
|
176
|
+
result: The tool result.
|
|
177
|
+
event_type: Discriminator field for type narrowing.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
tool_call_id: str
|
|
181
|
+
result: ToolResult
|
|
182
|
+
event_type: Literal["tool_end"] = "tool_end"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# =============================================================================
|
|
186
|
+
# Context Events
|
|
187
|
+
# =============================================================================
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class CompactionStartEvent(BaseEvent):
|
|
191
|
+
"""Event emitted when context compaction starts.
|
|
192
|
+
|
|
193
|
+
Attributes:
|
|
194
|
+
run_id: Unique identifier for the run.
|
|
195
|
+
message_count: Number of messages before compaction.
|
|
196
|
+
token_count: Number of tokens before compaction.
|
|
197
|
+
event_type: Discriminator field for type narrowing.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
message_count: int
|
|
201
|
+
token_count: int
|
|
202
|
+
event_type: Literal["compaction_start"] = "compaction_start"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class CompactionEndEvent(BaseEvent):
|
|
206
|
+
"""Event emitted when context compaction ends.
|
|
207
|
+
|
|
208
|
+
Attributes:
|
|
209
|
+
run_id: Unique identifier for the run.
|
|
210
|
+
messages_removed: Number of messages removed.
|
|
211
|
+
tokens_saved: Number of tokens saved.
|
|
212
|
+
event_type: Discriminator field for type narrowing.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
messages_removed: int
|
|
216
|
+
tokens_saved: int
|
|
217
|
+
event_type: Literal["compaction_end"] = "compaction_end"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# =============================================================================
|
|
221
|
+
# Union Type
|
|
222
|
+
# =============================================================================
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
StreamEventData = (
|
|
226
|
+
RunStartEvent
|
|
227
|
+
| RunEndEvent
|
|
228
|
+
| RunErrorEvent
|
|
229
|
+
| AssistantStartEvent
|
|
230
|
+
| TextDeltaEvent
|
|
231
|
+
| AssistantEndEvent
|
|
232
|
+
| ToolStartEvent
|
|
233
|
+
| ToolOutputEvent
|
|
234
|
+
| ToolEndEvent
|
|
235
|
+
| CompactionStartEvent
|
|
236
|
+
| CompactionEndEvent
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
__all__ = [
|
|
241
|
+
"AssistantEndEvent",
|
|
242
|
+
"AssistantStartEvent",
|
|
243
|
+
"BaseEvent",
|
|
244
|
+
"CompactionEndEvent",
|
|
245
|
+
"CompactionStartEvent",
|
|
246
|
+
"RunEndEvent",
|
|
247
|
+
"RunErrorEvent",
|
|
248
|
+
"RunStartEvent",
|
|
249
|
+
"StreamEventData",
|
|
250
|
+
"TextDeltaEvent",
|
|
251
|
+
"ToolEndEvent",
|
|
252
|
+
"ToolOutputEvent",
|
|
253
|
+
"ToolStartEvent",
|
|
254
|
+
]
|
|
255
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Sub-agent support for voxagent.
|
|
2
|
+
|
|
3
|
+
This module provides the ability to register agents as tools that can be called
|
|
4
|
+
by parent agents, enabling hierarchical agent composition and delegation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from voxagent.subagent.context import (
|
|
8
|
+
DEFAULT_MAX_DEPTH,
|
|
9
|
+
MaxDepthExceededError,
|
|
10
|
+
SubAgentContext,
|
|
11
|
+
)
|
|
12
|
+
from voxagent.subagent.definition import SubAgentDefinition
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SubAgentDefinition",
|
|
16
|
+
"SubAgentContext",
|
|
17
|
+
"MaxDepthExceededError",
|
|
18
|
+
"DEFAULT_MAX_DEPTH",
|
|
19
|
+
]
|
|
20
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Sub-agent context for depth tracking and propagation.
|
|
2
|
+
|
|
3
|
+
This module provides SubAgentContext which extends ToolContext with
|
|
4
|
+
depth tracking to prevent infinite recursion in nested agent calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
from pydantic import ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
from voxagent.providers.base import AbortSignal
|
|
14
|
+
from voxagent.tools.context import ToolContext
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
# Default maximum depth for nested agent calls
|
|
19
|
+
DEFAULT_MAX_DEPTH = 5
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MaxDepthExceededError(Exception):
|
|
23
|
+
"""Raised when sub-agent call exceeds maximum depth."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, depth: int, max_depth: int) -> None:
|
|
26
|
+
self.depth = depth
|
|
27
|
+
self.max_depth = max_depth
|
|
28
|
+
super().__init__(
|
|
29
|
+
f"Maximum sub-agent depth exceeded: {depth} > {max_depth}. "
|
|
30
|
+
"This may indicate infinite recursion or an overly deep agent hierarchy."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SubAgentContext(ToolContext[T]):
|
|
35
|
+
"""Extended context for sub-agent execution with depth tracking.
|
|
36
|
+
|
|
37
|
+
Extends ToolContext with:
|
|
38
|
+
- depth: Current nesting level (0 = root agent)
|
|
39
|
+
- max_depth: Maximum allowed nesting depth
|
|
40
|
+
- parent_run_id: Run ID of the parent agent (for tracing)
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
depth: Current depth in the agent hierarchy (0 = root).
|
|
44
|
+
max_depth: Maximum allowed depth before raising MaxDepthExceededError.
|
|
45
|
+
parent_run_id: The run_id of the parent agent that spawned this sub-agent.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
depth: int = Field(default=0, ge=0)
|
|
49
|
+
max_depth: int = Field(default=DEFAULT_MAX_DEPTH, ge=1)
|
|
50
|
+
parent_run_id: str | None = None
|
|
51
|
+
|
|
52
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
53
|
+
|
|
54
|
+
def check_depth(self) -> None:
|
|
55
|
+
"""Raise MaxDepthExceededError if depth exceeds max_depth."""
|
|
56
|
+
if self.depth >= self.max_depth:
|
|
57
|
+
raise MaxDepthExceededError(self.depth, self.max_depth)
|
|
58
|
+
|
|
59
|
+
def child_context(
|
|
60
|
+
self,
|
|
61
|
+
abort_signal: AbortSignal | None = None,
|
|
62
|
+
deps: Any = None,
|
|
63
|
+
session_id: str | None = None,
|
|
64
|
+
run_id: str | None = None,
|
|
65
|
+
) -> "SubAgentContext[Any]":
|
|
66
|
+
"""Create a child context with incremented depth.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
abort_signal: Abort signal for the child (uses parent's if None).
|
|
70
|
+
deps: Dependencies for the child (uses parent's if None).
|
|
71
|
+
session_id: Session ID for the child (uses parent's if None).
|
|
72
|
+
run_id: Run ID for the child agent.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
New SubAgentContext with depth + 1.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
MaxDepthExceededError: If the new depth exceeds max_depth.
|
|
79
|
+
"""
|
|
80
|
+
new_depth = self.depth + 1
|
|
81
|
+
if new_depth > self.max_depth:
|
|
82
|
+
raise MaxDepthExceededError(new_depth, self.max_depth)
|
|
83
|
+
|
|
84
|
+
return SubAgentContext(
|
|
85
|
+
abort_signal=abort_signal or self.abort_signal,
|
|
86
|
+
deps=deps if deps is not None else self.deps,
|
|
87
|
+
session_id=session_id or self.session_id,
|
|
88
|
+
run_id=run_id,
|
|
89
|
+
retry_count=0, # Reset retry count for child
|
|
90
|
+
depth=new_depth,
|
|
91
|
+
max_depth=self.max_depth,
|
|
92
|
+
parent_run_id=self.run_id, # Current run becomes parent
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_tool_context(
|
|
97
|
+
cls,
|
|
98
|
+
context: ToolContext,
|
|
99
|
+
depth: int = 0,
|
|
100
|
+
max_depth: int = DEFAULT_MAX_DEPTH,
|
|
101
|
+
parent_run_id: str | None = None,
|
|
102
|
+
) -> "SubAgentContext[Any]":
|
|
103
|
+
"""Create SubAgentContext from a regular ToolContext.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
context: The source ToolContext.
|
|
107
|
+
depth: Initial depth (default 0).
|
|
108
|
+
max_depth: Maximum depth (default DEFAULT_MAX_DEPTH).
|
|
109
|
+
parent_run_id: Parent run ID for tracing.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
New SubAgentContext with depth tracking.
|
|
113
|
+
"""
|
|
114
|
+
return cls(
|
|
115
|
+
abort_signal=context.abort_signal,
|
|
116
|
+
deps=context.deps,
|
|
117
|
+
session_id=context.session_id,
|
|
118
|
+
run_id=context.run_id,
|
|
119
|
+
retry_count=context.retry_count,
|
|
120
|
+
depth=depth,
|
|
121
|
+
max_depth=max_depth,
|
|
122
|
+
parent_run_id=parent_run_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Sub-agent definition for voxagent.
|
|
2
|
+
|
|
3
|
+
This module provides SubAgentDefinition, a ToolDefinition subclass that wraps
|
|
4
|
+
an Agent and makes it callable as a tool by a parent agent.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from voxagent.subagent.context import (
|
|
13
|
+
DEFAULT_MAX_DEPTH,
|
|
14
|
+
MaxDepthExceededError,
|
|
15
|
+
SubAgentContext,
|
|
16
|
+
)
|
|
17
|
+
from voxagent.tools.context import ToolContext
|
|
18
|
+
from voxagent.tools.definition import ToolDefinition
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from voxagent.agent.core import Agent
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SubAgentDefinition(ToolDefinition):
|
|
25
|
+
"""A ToolDefinition that wraps an Agent as a callable tool.
|
|
26
|
+
|
|
27
|
+
This allows parent agents to delegate tasks to specialized child agents.
|
|
28
|
+
The child agent inherits context (abort signal, deps, session) from parent.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
researcher = Agent(model="...", system_prompt="You research topics...")
|
|
32
|
+
|
|
33
|
+
parent = Agent(
|
|
34
|
+
model="...",
|
|
35
|
+
sub_agents=[researcher], # Auto-converted to SubAgentDefinition
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
agent: "Agent[Any, Any]",
|
|
42
|
+
name: str | None = None,
|
|
43
|
+
description: str | None = None,
|
|
44
|
+
max_depth: int = DEFAULT_MAX_DEPTH,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Initialize SubAgentDefinition.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
agent: The Agent instance to wrap as a tool.
|
|
50
|
+
name: Tool name (defaults to agent name or 'sub_agent').
|
|
51
|
+
description: Tool description (defaults to agent's system prompt).
|
|
52
|
+
max_depth: Maximum nesting depth for recursive calls.
|
|
53
|
+
"""
|
|
54
|
+
# Determine tool name
|
|
55
|
+
tool_name = name or getattr(agent, "name", None) or "sub_agent"
|
|
56
|
+
tool_name = self._sanitize_name(tool_name)
|
|
57
|
+
|
|
58
|
+
# Determine description
|
|
59
|
+
tool_desc = description or agent._system_prompt or f"Delegate to {tool_name}"
|
|
60
|
+
# Truncate long descriptions for tool schema
|
|
61
|
+
if len(tool_desc) > 500:
|
|
62
|
+
tool_desc = tool_desc[:497] + "..."
|
|
63
|
+
|
|
64
|
+
# Validate name manually (don't call parent __init__ with execute)
|
|
65
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", tool_name):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Tool name '{tool_name}' is invalid. "
|
|
68
|
+
"Must contain only alphanumeric characters and underscores."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self.name = tool_name
|
|
72
|
+
self.description = tool_desc
|
|
73
|
+
self.parameters = {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {
|
|
76
|
+
"task": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": "The task or query to delegate to this agent.",
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"required": ["task"],
|
|
82
|
+
}
|
|
83
|
+
self.is_async = True
|
|
84
|
+
|
|
85
|
+
# Sub-agent specific attributes
|
|
86
|
+
self._agent = agent
|
|
87
|
+
self._max_depth = max_depth
|
|
88
|
+
|
|
89
|
+
# Set execute placeholder
|
|
90
|
+
self.execute = self._execute_placeholder
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _sanitize_name(name: str) -> str:
|
|
94
|
+
"""Sanitize agent name to be a valid tool name."""
|
|
95
|
+
# Replace spaces and invalid chars with underscores
|
|
96
|
+
sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
97
|
+
# Ensure doesn't start with digit
|
|
98
|
+
if sanitized and sanitized[0].isdigit():
|
|
99
|
+
sanitized = "_" + sanitized
|
|
100
|
+
return sanitized or "sub_agent"
|
|
101
|
+
|
|
102
|
+
async def _execute_placeholder(self, **params: Any) -> Any:
|
|
103
|
+
"""Placeholder - actual execution happens in run()."""
|
|
104
|
+
raise NotImplementedError("Use run() instead")
|
|
105
|
+
|
|
106
|
+
async def run(self, params: dict[str, Any], context: ToolContext) -> Any:
|
|
107
|
+
"""Execute the sub-agent with the given task.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
params: Must contain 'task' key with the prompt for the sub-agent.
|
|
111
|
+
context: The ToolContext from the parent agent.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
The sub-agent's response text.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
MaxDepthExceededError: If nesting depth exceeds max_depth.
|
|
118
|
+
"""
|
|
119
|
+
task = params.get("task", "")
|
|
120
|
+
if not task:
|
|
121
|
+
return "Error: No task provided for sub-agent"
|
|
122
|
+
|
|
123
|
+
# Convert or get SubAgentContext with depth tracking
|
|
124
|
+
if isinstance(context, SubAgentContext):
|
|
125
|
+
sub_context = context
|
|
126
|
+
else:
|
|
127
|
+
sub_context = SubAgentContext.from_tool_context(
|
|
128
|
+
context, depth=0, max_depth=self._max_depth
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Check depth before spawning child
|
|
132
|
+
try:
|
|
133
|
+
child_context = sub_context.child_context(
|
|
134
|
+
run_id=None, # Agent.run() will create new run_id
|
|
135
|
+
)
|
|
136
|
+
except MaxDepthExceededError as e:
|
|
137
|
+
return f"Error: {e}"
|
|
138
|
+
|
|
139
|
+
# Run the sub-agent
|
|
140
|
+
try:
|
|
141
|
+
print(f"\n🤖 SUB-AGENT CALLED: {self.name}")
|
|
142
|
+
print(f" Task: {task[:100]}{'...' if len(task) > 100 else ''}")
|
|
143
|
+
print(f" Depth: {child_context.depth}/{child_context.max_depth}")
|
|
144
|
+
|
|
145
|
+
result = await self._agent.run(
|
|
146
|
+
prompt=task,
|
|
147
|
+
deps=child_context.deps,
|
|
148
|
+
session_key=child_context.session_id,
|
|
149
|
+
# Pass depth info via message_history metadata (optional)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if result.error:
|
|
153
|
+
return f"Sub-agent error: {result.error}"
|
|
154
|
+
|
|
155
|
+
# Return the agent's response
|
|
156
|
+
if result.assistant_texts:
|
|
157
|
+
return result.assistant_texts[-1] # Last response
|
|
158
|
+
return "Sub-agent completed with no response"
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return f"Sub-agent execution error: {type(e).__name__}: {e}"
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def agent(self) -> "Agent[Any, Any]":
|
|
165
|
+
"""Get the wrapped Agent instance."""
|
|
166
|
+
return self._agent
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def max_depth(self) -> int:
|
|
170
|
+
"""Get the maximum nesting depth."""
|
|
171
|
+
return self._max_depth
|
|
172
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Tool system for agent tool definitions and execution.
|
|
2
|
+
|
|
3
|
+
This subpackage provides:
|
|
4
|
+
- Tool definitions with typed parameters
|
|
5
|
+
- Tool registry for dynamic registration
|
|
6
|
+
- Tool policies for allow/deny filtering
|
|
7
|
+
- Tool execution with abort signal support
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from voxagent.tools.context import AbortError, ToolContext
|
|
11
|
+
from voxagent.tools.decorator import tool
|
|
12
|
+
from voxagent.tools.definition import ToolDefinition
|
|
13
|
+
from voxagent.tools.executor import execute_tool
|
|
14
|
+
from voxagent.tools.policy import ToolPolicy, apply_tool_policies
|
|
15
|
+
from voxagent.tools.registry import (
|
|
16
|
+
ToolAlreadyRegisteredError,
|
|
17
|
+
ToolNotFoundError,
|
|
18
|
+
ToolRegistry,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AbortError",
|
|
23
|
+
"ToolAlreadyRegisteredError",
|
|
24
|
+
"ToolContext",
|
|
25
|
+
"ToolDefinition",
|
|
26
|
+
"ToolNotFoundError",
|
|
27
|
+
"ToolPolicy",
|
|
28
|
+
"ToolRegistry",
|
|
29
|
+
"apply_tool_policies",
|
|
30
|
+
"execute_tool",
|
|
31
|
+
"tool",
|
|
32
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Tool context for voxagent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
from voxagent.providers.base import AbortSignal
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AbortError(Exception):
|
|
15
|
+
"""Raised when an operation is aborted."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str = "Operation aborted") -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ToolContext(BaseModel, Generic[T]):
|
|
22
|
+
"""Runtime context passed to tool functions.
|
|
23
|
+
|
|
24
|
+
Replaces PydanticAI's RunContext with a simpler, more focused API.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
abort_signal: Signal to check for abort requests
|
|
28
|
+
deps: Optional dependencies injected by the agent
|
|
29
|
+
session_id: Current session ID
|
|
30
|
+
run_id: Current run ID
|
|
31
|
+
retry_count: Number of times this tool has been retried
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
abort_signal: AbortSignal
|
|
35
|
+
deps: T | None = None
|
|
36
|
+
session_id: str | None = None
|
|
37
|
+
run_id: str | None = None
|
|
38
|
+
retry_count: int = 0
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
41
|
+
|
|
42
|
+
def is_aborted(self) -> bool:
|
|
43
|
+
"""Check if the operation has been aborted."""
|
|
44
|
+
return self.abort_signal.aborted
|
|
45
|
+
|
|
46
|
+
def check_abort(self) -> None:
|
|
47
|
+
"""Raise AbortError if aborted."""
|
|
48
|
+
if self.abort_signal.aborted:
|
|
49
|
+
raise AbortError("Operation aborted")
|
|
50
|
+
|