agent-runtime-core 0.7.0__py3-none-any.whl → 0.8.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.
- agent_runtime_core/__init__.py +109 -2
- agent_runtime_core/agentic_loop.py +254 -0
- agent_runtime_core/config.py +54 -4
- agent_runtime_core/config_schema.py +307 -0
- agent_runtime_core/files/__init__.py +88 -0
- agent_runtime_core/files/base.py +343 -0
- agent_runtime_core/files/ocr.py +406 -0
- agent_runtime_core/files/processors.py +508 -0
- agent_runtime_core/files/tools.py +317 -0
- agent_runtime_core/files/vision.py +360 -0
- agent_runtime_core/interfaces.py +106 -0
- agent_runtime_core/json_runtime.py +509 -0
- agent_runtime_core/llm/__init__.py +80 -7
- agent_runtime_core/llm/anthropic.py +133 -12
- agent_runtime_core/llm/models_config.py +180 -0
- agent_runtime_core/memory/__init__.py +70 -0
- agent_runtime_core/memory/manager.py +554 -0
- agent_runtime_core/memory/mixin.py +294 -0
- agent_runtime_core/multi_agent.py +569 -0
- agent_runtime_core/persistence/__init__.py +2 -0
- agent_runtime_core/persistence/file.py +277 -0
- agent_runtime_core/rag/__init__.py +65 -0
- agent_runtime_core/rag/chunking.py +224 -0
- agent_runtime_core/rag/indexer.py +253 -0
- agent_runtime_core/rag/retriever.py +261 -0
- agent_runtime_core/runner.py +193 -15
- agent_runtime_core/tool_calling_agent.py +88 -130
- agent_runtime_core/tools.py +179 -0
- agent_runtime_core/vectorstore/__init__.py +193 -0
- agent_runtime_core/vectorstore/base.py +138 -0
- agent_runtime_core/vectorstore/embeddings.py +242 -0
- agent_runtime_core/vectorstore/sqlite_vec.py +328 -0
- agent_runtime_core/vectorstore/vertex.py +295 -0
- {agent_runtime_core-0.7.0.dist-info → agent_runtime_core-0.8.0.dist-info}/METADATA +236 -1
- agent_runtime_core-0.8.0.dist-info/RECORD +63 -0
- agent_runtime_core-0.7.0.dist-info/RECORD +0 -39
- {agent_runtime_core-0.7.0.dist-info → agent_runtime_core-0.8.0.dist-info}/WHEEL +0 -0
- {agent_runtime_core-0.7.0.dist-info → agent_runtime_core-0.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-agent support for agent_runtime_core.
|
|
3
|
+
|
|
4
|
+
This module provides the "agent-as-tool" pattern, allowing agents to invoke
|
|
5
|
+
other agents as tools. This enables:
|
|
6
|
+
- Router/dispatcher patterns
|
|
7
|
+
- Hierarchical agent systems
|
|
8
|
+
- Specialist delegation
|
|
9
|
+
|
|
10
|
+
Two invocation modes are supported:
|
|
11
|
+
- DELEGATE: Sub-agent runs and returns result to parent (parent continues)
|
|
12
|
+
- HANDOFF: Control transfers completely to sub-agent (parent exits)
|
|
13
|
+
|
|
14
|
+
Context passing is configurable:
|
|
15
|
+
- FULL: Complete conversation history passed to sub-agent (default)
|
|
16
|
+
- SUMMARY: Summarized context + current message
|
|
17
|
+
- MESSAGE_ONLY: Only the invocation message
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
from agent_runtime_core.multi_agent import (
|
|
21
|
+
AgentTool,
|
|
22
|
+
InvocationMode,
|
|
23
|
+
ContextMode,
|
|
24
|
+
invoke_agent,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Define a sub-agent as a tool
|
|
28
|
+
billing_agent_tool = AgentTool(
|
|
29
|
+
agent=billing_agent,
|
|
30
|
+
name="billing_specialist",
|
|
31
|
+
description="Handles billing questions, refunds, and payment issues",
|
|
32
|
+
invocation_mode=InvocationMode.DELEGATE,
|
|
33
|
+
context_mode=ContextMode.FULL,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Invoke it
|
|
37
|
+
result = await invoke_agent(
|
|
38
|
+
agent_tool=billing_agent_tool,
|
|
39
|
+
message="Customer wants a refund for order #123",
|
|
40
|
+
parent_ctx=ctx,
|
|
41
|
+
conversation_history=messages,
|
|
42
|
+
)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import logging
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from enum import Enum
|
|
48
|
+
from typing import Any, Callable, Optional, Protocol, TYPE_CHECKING
|
|
49
|
+
from uuid import UUID, uuid4
|
|
50
|
+
|
|
51
|
+
from agent_runtime_core.interfaces import (
|
|
52
|
+
AgentRuntime,
|
|
53
|
+
Message,
|
|
54
|
+
RunContext,
|
|
55
|
+
RunResult,
|
|
56
|
+
Tool,
|
|
57
|
+
ToolRegistry,
|
|
58
|
+
EventType,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from agent_runtime_core.contexts import InMemoryRunContext
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class InvocationMode(str, Enum):
|
|
68
|
+
"""
|
|
69
|
+
How the sub-agent is invoked.
|
|
70
|
+
|
|
71
|
+
DELEGATE: Sub-agent runs, returns result to parent. Parent continues
|
|
72
|
+
its execution with the result. Good for "get me an answer".
|
|
73
|
+
|
|
74
|
+
HANDOFF: Control transfers completely to sub-agent. Parent's run ends
|
|
75
|
+
and sub-agent takes over the conversation. Good for "transfer
|
|
76
|
+
this customer to billing".
|
|
77
|
+
"""
|
|
78
|
+
DELEGATE = "delegate"
|
|
79
|
+
HANDOFF = "handoff"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ContextMode(str, Enum):
|
|
83
|
+
"""
|
|
84
|
+
What context is passed to the sub-agent.
|
|
85
|
+
|
|
86
|
+
FULL: Complete conversation history. Sub-agent sees everything the
|
|
87
|
+
parent has seen. Best for sensitive contexts where nothing
|
|
88
|
+
should be forgotten. This is the default.
|
|
89
|
+
|
|
90
|
+
SUMMARY: A summary of the conversation + the current message.
|
|
91
|
+
More efficient but may lose nuance.
|
|
92
|
+
|
|
93
|
+
MESSAGE_ONLY: Only the invocation message. Clean isolation but
|
|
94
|
+
sub-agent lacks context.
|
|
95
|
+
"""
|
|
96
|
+
FULL = "full"
|
|
97
|
+
SUMMARY = "summary"
|
|
98
|
+
MESSAGE_ONLY = "message_only"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class AgentTool:
|
|
103
|
+
"""
|
|
104
|
+
Wraps an agent to be used as a tool by another agent.
|
|
105
|
+
|
|
106
|
+
This is the core abstraction for multi-agent systems. Any agent can
|
|
107
|
+
be wrapped as a tool and added to another agent's tool registry.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
agent: The agent runtime to invoke
|
|
111
|
+
name: Tool name (how the parent agent calls it)
|
|
112
|
+
description: When to use this agent (shown to parent LLM)
|
|
113
|
+
invocation_mode: DELEGATE or HANDOFF
|
|
114
|
+
context_mode: How much context to pass (FULL, SUMMARY, MESSAGE_ONLY)
|
|
115
|
+
max_turns: Optional limit on sub-agent turns (for DELEGATE mode)
|
|
116
|
+
input_schema: Optional custom input schema (defaults to message + context)
|
|
117
|
+
metadata: Additional metadata for the tool
|
|
118
|
+
"""
|
|
119
|
+
agent: AgentRuntime
|
|
120
|
+
name: str
|
|
121
|
+
description: str
|
|
122
|
+
invocation_mode: InvocationMode = InvocationMode.DELEGATE
|
|
123
|
+
context_mode: ContextMode = ContextMode.FULL
|
|
124
|
+
max_turns: Optional[int] = None
|
|
125
|
+
input_schema: Optional[dict] = None
|
|
126
|
+
metadata: dict = field(default_factory=dict)
|
|
127
|
+
|
|
128
|
+
def to_tool_schema(self) -> dict:
|
|
129
|
+
"""
|
|
130
|
+
Generate the OpenAI-format tool schema for this agent-tool.
|
|
131
|
+
|
|
132
|
+
The schema allows the parent agent to invoke this sub-agent
|
|
133
|
+
with a message and optional context override.
|
|
134
|
+
"""
|
|
135
|
+
if self.input_schema:
|
|
136
|
+
parameters = self.input_schema
|
|
137
|
+
else:
|
|
138
|
+
# Default schema: message + optional context
|
|
139
|
+
parameters = {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"message": {
|
|
143
|
+
"type": "string",
|
|
144
|
+
"description": "The message or task to send to this agent",
|
|
145
|
+
},
|
|
146
|
+
"context": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"description": "Optional additional context to include",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
"required": ["message"],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"type": "function",
|
|
156
|
+
"function": {
|
|
157
|
+
"name": self.name,
|
|
158
|
+
"description": self.description,
|
|
159
|
+
"parameters": parameters,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class AgentInvocationResult:
|
|
166
|
+
"""
|
|
167
|
+
Result from invoking a sub-agent.
|
|
168
|
+
|
|
169
|
+
Attributes:
|
|
170
|
+
response: The sub-agent's final response text
|
|
171
|
+
messages: All messages from the sub-agent's run
|
|
172
|
+
handoff: True if this was a handoff (parent should exit)
|
|
173
|
+
run_result: The full RunResult from the sub-agent
|
|
174
|
+
sub_agent_key: The key of the agent that was invoked
|
|
175
|
+
"""
|
|
176
|
+
response: str
|
|
177
|
+
messages: list[Message]
|
|
178
|
+
handoff: bool
|
|
179
|
+
run_result: RunResult
|
|
180
|
+
sub_agent_key: str
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class SubAgentContext:
|
|
184
|
+
"""
|
|
185
|
+
RunContext implementation for sub-agent invocations.
|
|
186
|
+
|
|
187
|
+
Wraps the parent context but with modified input messages
|
|
188
|
+
based on the context mode.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
parent_ctx: RunContext,
|
|
194
|
+
input_messages: list[Message],
|
|
195
|
+
sub_run_id: Optional[UUID] = None,
|
|
196
|
+
):
|
|
197
|
+
self._parent_ctx = parent_ctx
|
|
198
|
+
self._input_messages = input_messages
|
|
199
|
+
self._run_id = sub_run_id or uuid4()
|
|
200
|
+
self._state: Optional[dict] = None
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def run_id(self) -> UUID:
|
|
204
|
+
return self._run_id
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def conversation_id(self) -> Optional[UUID]:
|
|
208
|
+
# Sub-agent shares the same conversation
|
|
209
|
+
return self._parent_ctx.conversation_id
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def input_messages(self) -> list[Message]:
|
|
213
|
+
return self._input_messages
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def params(self) -> dict:
|
|
217
|
+
return self._parent_ctx.params
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def metadata(self) -> dict:
|
|
221
|
+
# Add sub-agent metadata
|
|
222
|
+
meta = dict(self._parent_ctx.metadata)
|
|
223
|
+
meta["parent_run_id"] = str(self._parent_ctx.run_id)
|
|
224
|
+
meta["is_sub_agent"] = True
|
|
225
|
+
return meta
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def tool_registry(self) -> ToolRegistry:
|
|
229
|
+
# Sub-agent uses its own tools, not parent's
|
|
230
|
+
# This is set by the agent itself
|
|
231
|
+
return ToolRegistry()
|
|
232
|
+
|
|
233
|
+
async def emit(self, event_type: EventType | str, payload: dict) -> None:
|
|
234
|
+
"""Emit events through parent context with sub-agent tagging."""
|
|
235
|
+
# Tag the event as coming from a sub-agent
|
|
236
|
+
tagged_payload = dict(payload)
|
|
237
|
+
tagged_payload["sub_agent_run_id"] = str(self._run_id)
|
|
238
|
+
tagged_payload["parent_run_id"] = str(self._parent_ctx.run_id)
|
|
239
|
+
await self._parent_ctx.emit(event_type, tagged_payload)
|
|
240
|
+
|
|
241
|
+
async def emit_user_message(self, content: str) -> None:
|
|
242
|
+
"""Emit a user-visible message."""
|
|
243
|
+
await self.emit(EventType.ASSISTANT_MESSAGE, {
|
|
244
|
+
"content": content,
|
|
245
|
+
"role": "assistant",
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
async def emit_error(self, error: str, details: dict = None) -> None:
|
|
249
|
+
"""Emit an error event."""
|
|
250
|
+
await self.emit(EventType.ERROR, {
|
|
251
|
+
"error": error,
|
|
252
|
+
"details": details or {},
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
async def checkpoint(self, state: dict) -> None:
|
|
256
|
+
"""Save state checkpoint."""
|
|
257
|
+
self._state = state
|
|
258
|
+
# Could also delegate to parent for persistence
|
|
259
|
+
|
|
260
|
+
async def get_state(self) -> Optional[dict]:
|
|
261
|
+
"""Get last checkpointed state."""
|
|
262
|
+
return self._state
|
|
263
|
+
|
|
264
|
+
def cancelled(self) -> bool:
|
|
265
|
+
"""Check if parent has been cancelled."""
|
|
266
|
+
return self._parent_ctx.cancelled()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def build_sub_agent_messages(
|
|
270
|
+
context_mode: ContextMode,
|
|
271
|
+
message: str,
|
|
272
|
+
conversation_history: list[Message],
|
|
273
|
+
additional_context: Optional[str] = None,
|
|
274
|
+
) -> list[Message]:
|
|
275
|
+
"""
|
|
276
|
+
Build the input messages for a sub-agent based on context mode.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
context_mode: How much context to include
|
|
280
|
+
message: The invocation message from the parent
|
|
281
|
+
conversation_history: Full conversation history from parent
|
|
282
|
+
additional_context: Optional extra context string
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List of messages to pass to the sub-agent
|
|
286
|
+
"""
|
|
287
|
+
if context_mode == ContextMode.FULL:
|
|
288
|
+
# Include full history + new message
|
|
289
|
+
messages = list(conversation_history)
|
|
290
|
+
|
|
291
|
+
# Build the user message
|
|
292
|
+
user_content = message
|
|
293
|
+
if additional_context:
|
|
294
|
+
user_content = f"{additional_context}\n\n{message}"
|
|
295
|
+
|
|
296
|
+
messages.append({
|
|
297
|
+
"role": "user",
|
|
298
|
+
"content": user_content,
|
|
299
|
+
})
|
|
300
|
+
return messages
|
|
301
|
+
|
|
302
|
+
elif context_mode == ContextMode.SUMMARY:
|
|
303
|
+
# TODO: Implement summarization
|
|
304
|
+
# For now, fall back to including last few messages
|
|
305
|
+
recent_messages = conversation_history[-5:] if conversation_history else []
|
|
306
|
+
|
|
307
|
+
user_content = message
|
|
308
|
+
if additional_context:
|
|
309
|
+
user_content = f"{additional_context}\n\n{message}"
|
|
310
|
+
|
|
311
|
+
return list(recent_messages) + [{
|
|
312
|
+
"role": "user",
|
|
313
|
+
"content": user_content,
|
|
314
|
+
}]
|
|
315
|
+
|
|
316
|
+
else: # MESSAGE_ONLY
|
|
317
|
+
user_content = message
|
|
318
|
+
if additional_context:
|
|
319
|
+
user_content = f"{additional_context}\n\n{message}"
|
|
320
|
+
|
|
321
|
+
return [{
|
|
322
|
+
"role": "user",
|
|
323
|
+
"content": user_content,
|
|
324
|
+
}]
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
async def invoke_agent(
|
|
328
|
+
agent_tool: AgentTool,
|
|
329
|
+
message: str,
|
|
330
|
+
parent_ctx: RunContext,
|
|
331
|
+
conversation_history: Optional[list[Message]] = None,
|
|
332
|
+
additional_context: Optional[str] = None,
|
|
333
|
+
) -> AgentInvocationResult:
|
|
334
|
+
"""
|
|
335
|
+
Invoke a sub-agent as a tool.
|
|
336
|
+
|
|
337
|
+
This is the main entry point for agent-to-agent invocation.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
agent_tool: The AgentTool wrapping the sub-agent
|
|
341
|
+
message: The message/task to send to the sub-agent
|
|
342
|
+
parent_ctx: The parent agent's run context
|
|
343
|
+
conversation_history: Full conversation history (for FULL context mode)
|
|
344
|
+
additional_context: Optional extra context to include
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
AgentInvocationResult with the sub-agent's response
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
result = await invoke_agent(
|
|
351
|
+
agent_tool=billing_specialist,
|
|
352
|
+
message="Process refund for order #123",
|
|
353
|
+
parent_ctx=ctx,
|
|
354
|
+
conversation_history=messages,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if result.handoff:
|
|
358
|
+
# Sub-agent took over, return its result
|
|
359
|
+
return RunResult(
|
|
360
|
+
final_output=result.run_result.final_output,
|
|
361
|
+
final_messages=result.messages,
|
|
362
|
+
)
|
|
363
|
+
else:
|
|
364
|
+
# Got a response, continue parent execution
|
|
365
|
+
print(f"Billing says: {result.response}")
|
|
366
|
+
"""
|
|
367
|
+
logger.info(
|
|
368
|
+
f"Invoking sub-agent '{agent_tool.name}' "
|
|
369
|
+
f"(mode={agent_tool.invocation_mode.value}, "
|
|
370
|
+
f"context={agent_tool.context_mode.value})"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Build messages based on context mode
|
|
374
|
+
history = conversation_history or []
|
|
375
|
+
sub_messages = build_sub_agent_messages(
|
|
376
|
+
context_mode=agent_tool.context_mode,
|
|
377
|
+
message=message,
|
|
378
|
+
conversation_history=history,
|
|
379
|
+
additional_context=additional_context,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Create sub-agent context
|
|
383
|
+
sub_ctx = SubAgentContext(
|
|
384
|
+
parent_ctx=parent_ctx,
|
|
385
|
+
input_messages=sub_messages,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Emit event for sub-agent invocation (tool call format)
|
|
389
|
+
await parent_ctx.emit(EventType.TOOL_CALL, {
|
|
390
|
+
"name": agent_tool.name,
|
|
391
|
+
"arguments": {"message": message, "context": additional_context},
|
|
392
|
+
"is_agent_tool": True,
|
|
393
|
+
"sub_agent_key": agent_tool.agent.key,
|
|
394
|
+
"invocation_mode": agent_tool.invocation_mode.value,
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
# Also emit a custom sub_agent.start event for UI display
|
|
398
|
+
# Get agent name from the agent if available
|
|
399
|
+
agent_name = getattr(agent_tool.agent, 'name', None) or agent_tool.name
|
|
400
|
+
await parent_ctx.emit("sub_agent.start", {
|
|
401
|
+
"sub_agent_key": agent_tool.agent.key,
|
|
402
|
+
"agent_name": agent_name,
|
|
403
|
+
"tool_name": agent_tool.name,
|
|
404
|
+
"invocation_mode": agent_tool.invocation_mode.value,
|
|
405
|
+
"context_mode": agent_tool.context_mode.value,
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
# Run the sub-agent
|
|
409
|
+
try:
|
|
410
|
+
run_result = await agent_tool.agent.run(sub_ctx)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.exception(f"Sub-agent '{agent_tool.name}' failed")
|
|
413
|
+
# Emit error event
|
|
414
|
+
await parent_ctx.emit(EventType.TOOL_RESULT, {
|
|
415
|
+
"name": agent_tool.name,
|
|
416
|
+
"is_agent_tool": True,
|
|
417
|
+
"error": str(e),
|
|
418
|
+
})
|
|
419
|
+
# Emit sub_agent.end with error
|
|
420
|
+
await parent_ctx.emit("sub_agent.end", {
|
|
421
|
+
"sub_agent_key": agent_tool.agent.key,
|
|
422
|
+
"agent_name": agent_name,
|
|
423
|
+
"tool_name": agent_tool.name,
|
|
424
|
+
"error": str(e),
|
|
425
|
+
})
|
|
426
|
+
raise
|
|
427
|
+
|
|
428
|
+
# Extract response
|
|
429
|
+
response = run_result.final_output.get("response", "")
|
|
430
|
+
if not response and run_result.final_messages:
|
|
431
|
+
# Try to get from last assistant message
|
|
432
|
+
for msg in reversed(run_result.final_messages):
|
|
433
|
+
if msg.get("role") == "assistant" and msg.get("content"):
|
|
434
|
+
response = msg["content"]
|
|
435
|
+
break
|
|
436
|
+
|
|
437
|
+
# Emit result event (tool result format)
|
|
438
|
+
await parent_ctx.emit(EventType.TOOL_RESULT, {
|
|
439
|
+
"name": agent_tool.name,
|
|
440
|
+
"is_agent_tool": True,
|
|
441
|
+
"sub_agent_key": agent_tool.agent.key,
|
|
442
|
+
"response": response[:500] if response else "", # Truncate for event
|
|
443
|
+
"handoff": agent_tool.invocation_mode == InvocationMode.HANDOFF,
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
# Also emit a custom sub_agent.end event for UI display
|
|
447
|
+
await parent_ctx.emit("sub_agent.end", {
|
|
448
|
+
"sub_agent_key": agent_tool.agent.key,
|
|
449
|
+
"agent_name": agent_name,
|
|
450
|
+
"tool_name": agent_tool.name,
|
|
451
|
+
"success": True,
|
|
452
|
+
"handoff": agent_tool.invocation_mode == InvocationMode.HANDOFF,
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
return AgentInvocationResult(
|
|
456
|
+
response=response,
|
|
457
|
+
messages=run_result.final_messages,
|
|
458
|
+
handoff=agent_tool.invocation_mode == InvocationMode.HANDOFF,
|
|
459
|
+
run_result=run_result,
|
|
460
|
+
sub_agent_key=agent_tool.agent.key,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def create_agent_tool_handler(
|
|
465
|
+
agent_tool: AgentTool,
|
|
466
|
+
get_conversation_history: Callable[[], list[Message]],
|
|
467
|
+
parent_ctx: RunContext,
|
|
468
|
+
) -> Callable:
|
|
469
|
+
"""
|
|
470
|
+
Create a tool handler function for an AgentTool.
|
|
471
|
+
|
|
472
|
+
This creates a handler that can be registered in a ToolRegistry,
|
|
473
|
+
allowing the agent-tool to be called like any other tool.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
agent_tool: The AgentTool to create a handler for
|
|
477
|
+
get_conversation_history: Function that returns current conversation
|
|
478
|
+
parent_ctx: The parent agent's context
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Async handler function compatible with ToolRegistry
|
|
482
|
+
|
|
483
|
+
Example:
|
|
484
|
+
handler = create_agent_tool_handler(
|
|
485
|
+
billing_agent_tool,
|
|
486
|
+
lambda: current_messages,
|
|
487
|
+
ctx,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
registry.register(Tool(
|
|
491
|
+
name=billing_agent_tool.name,
|
|
492
|
+
description=billing_agent_tool.description,
|
|
493
|
+
parameters=billing_agent_tool.to_tool_schema()["function"]["parameters"],
|
|
494
|
+
handler=handler,
|
|
495
|
+
))
|
|
496
|
+
"""
|
|
497
|
+
async def handler(message: str, context: Optional[str] = None) -> dict:
|
|
498
|
+
result = await invoke_agent(
|
|
499
|
+
agent_tool=agent_tool,
|
|
500
|
+
message=message,
|
|
501
|
+
parent_ctx=parent_ctx,
|
|
502
|
+
conversation_history=get_conversation_history(),
|
|
503
|
+
additional_context=context,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
if result.handoff:
|
|
507
|
+
# Signal to the parent that this is a handoff
|
|
508
|
+
return {
|
|
509
|
+
"handoff": True,
|
|
510
|
+
"response": result.response,
|
|
511
|
+
"sub_agent": result.sub_agent_key,
|
|
512
|
+
"final_output": result.run_result.final_output,
|
|
513
|
+
}
|
|
514
|
+
else:
|
|
515
|
+
return {
|
|
516
|
+
"response": result.response,
|
|
517
|
+
"sub_agent": result.sub_agent_key,
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return handler
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def register_agent_tools(
|
|
524
|
+
registry: ToolRegistry,
|
|
525
|
+
agent_tools: list[AgentTool],
|
|
526
|
+
get_conversation_history: Callable[[], list[Message]],
|
|
527
|
+
parent_ctx: RunContext,
|
|
528
|
+
) -> None:
|
|
529
|
+
"""
|
|
530
|
+
Register multiple agent-tools in a ToolRegistry.
|
|
531
|
+
|
|
532
|
+
Convenience function to add several sub-agents as tools.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
registry: The ToolRegistry to add tools to
|
|
536
|
+
agent_tools: List of AgentTools to register
|
|
537
|
+
get_conversation_history: Function returning current conversation
|
|
538
|
+
parent_ctx: Parent agent's context
|
|
539
|
+
|
|
540
|
+
Example:
|
|
541
|
+
register_agent_tools(
|
|
542
|
+
registry=self.tools,
|
|
543
|
+
agent_tools=[billing_agent_tool, support_agent_tool],
|
|
544
|
+
get_conversation_history=lambda: self._messages,
|
|
545
|
+
parent_ctx=ctx,
|
|
546
|
+
)
|
|
547
|
+
"""
|
|
548
|
+
for agent_tool in agent_tools:
|
|
549
|
+
handler = create_agent_tool_handler(
|
|
550
|
+
agent_tool=agent_tool,
|
|
551
|
+
get_conversation_history=get_conversation_history,
|
|
552
|
+
parent_ctx=parent_ctx,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
schema = agent_tool.to_tool_schema()
|
|
556
|
+
|
|
557
|
+
registry.register(Tool(
|
|
558
|
+
name=agent_tool.name,
|
|
559
|
+
description=agent_tool.description,
|
|
560
|
+
parameters=schema["function"]["parameters"],
|
|
561
|
+
handler=handler,
|
|
562
|
+
metadata={
|
|
563
|
+
"is_agent_tool": True,
|
|
564
|
+
"sub_agent_key": agent_tool.agent.key,
|
|
565
|
+
"invocation_mode": agent_tool.invocation_mode.value,
|
|
566
|
+
"context_mode": agent_tool.context_mode.value,
|
|
567
|
+
},
|
|
568
|
+
))
|
|
569
|
+
|
|
@@ -70,6 +70,7 @@ from agent_runtime_core.persistence.file import (
|
|
|
70
70
|
FileConversationStore,
|
|
71
71
|
FileTaskStore,
|
|
72
72
|
FilePreferencesStore,
|
|
73
|
+
FileKnowledgeStore,
|
|
73
74
|
)
|
|
74
75
|
|
|
75
76
|
from agent_runtime_core.persistence.manager import (
|
|
@@ -115,6 +116,7 @@ __all__ = [
|
|
|
115
116
|
"FileConversationStore",
|
|
116
117
|
"FileTaskStore",
|
|
117
118
|
"FilePreferencesStore",
|
|
119
|
+
"FileKnowledgeStore",
|
|
118
120
|
# Manager
|
|
119
121
|
"PersistenceManager",
|
|
120
122
|
"PersistenceConfig",
|