agent-runtime-core 0.6.0__py3-none-any.whl → 0.7.1__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 (33) hide show
  1. agent_runtime_core/__init__.py +118 -2
  2. agent_runtime_core/agentic_loop.py +254 -0
  3. agent_runtime_core/config.py +54 -4
  4. agent_runtime_core/config_schema.py +307 -0
  5. agent_runtime_core/contexts.py +348 -0
  6. agent_runtime_core/interfaces.py +106 -0
  7. agent_runtime_core/json_runtime.py +509 -0
  8. agent_runtime_core/llm/__init__.py +80 -7
  9. agent_runtime_core/llm/anthropic.py +133 -12
  10. agent_runtime_core/llm/models_config.py +180 -0
  11. agent_runtime_core/memory/__init__.py +70 -0
  12. agent_runtime_core/memory/manager.py +554 -0
  13. agent_runtime_core/memory/mixin.py +294 -0
  14. agent_runtime_core/multi_agent.py +569 -0
  15. agent_runtime_core/persistence/__init__.py +2 -0
  16. agent_runtime_core/persistence/file.py +277 -0
  17. agent_runtime_core/rag/__init__.py +65 -0
  18. agent_runtime_core/rag/chunking.py +224 -0
  19. agent_runtime_core/rag/indexer.py +253 -0
  20. agent_runtime_core/rag/retriever.py +261 -0
  21. agent_runtime_core/runner.py +193 -15
  22. agent_runtime_core/tool_calling_agent.py +88 -130
  23. agent_runtime_core/tools.py +179 -0
  24. agent_runtime_core/vectorstore/__init__.py +193 -0
  25. agent_runtime_core/vectorstore/base.py +138 -0
  26. agent_runtime_core/vectorstore/embeddings.py +242 -0
  27. agent_runtime_core/vectorstore/sqlite_vec.py +328 -0
  28. agent_runtime_core/vectorstore/vertex.py +295 -0
  29. {agent_runtime_core-0.6.0.dist-info → agent_runtime_core-0.7.1.dist-info}/METADATA +202 -1
  30. agent_runtime_core-0.7.1.dist-info/RECORD +57 -0
  31. agent_runtime_core-0.6.0.dist-info/RECORD +0 -38
  32. {agent_runtime_core-0.6.0.dist-info → agent_runtime_core-0.7.1.dist-info}/WHEEL +0 -0
  33. {agent_runtime_core-0.6.0.dist-info → agent_runtime_core-0.7.1.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",