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,97 @@
1
+ """State isolation for SubAgent execution.
2
+
3
+ Provides ChainMap-based state isolation for EMBEDDED mode SubAgents.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from abc import abstractmethod
8
+ from collections import ChainMap
9
+ from typing import Any, Protocol
10
+
11
+
12
+ class StateIsolator(Protocol):
13
+ """Protocol for state isolation.
14
+
15
+ Used to isolate state changes in EMBEDDED SubAgent mode.
16
+ Changes in child scope don't affect parent until merged.
17
+ """
18
+
19
+ @abstractmethod
20
+ def create_branch(self) -> "StateIsolator":
21
+ """Create a child branch with isolated state."""
22
+ ...
23
+
24
+ @abstractmethod
25
+ def get(self, key: str, default: Any = None) -> Any:
26
+ """Get value from state."""
27
+ ...
28
+
29
+ @abstractmethod
30
+ def set(self, key: str, value: Any) -> None:
31
+ """Set value in current scope (doesn't affect parent)."""
32
+ ...
33
+
34
+ @abstractmethod
35
+ def get_local_changes(self) -> dict[str, Any]:
36
+ """Get changes made in current scope only."""
37
+ ...
38
+
39
+ @abstractmethod
40
+ def merge_to_parent(self) -> None:
41
+ """Merge local changes to parent scope."""
42
+ ...
43
+
44
+
45
+ class ChainMapIsolator:
46
+ """ChainMap-based state isolator.
47
+
48
+ Uses ChainMap to provide copy-on-write semantics.
49
+ Child writes go to a new dict, reads fall through to parent.
50
+ """
51
+
52
+ def __init__(self, parent: "ChainMapIsolator | None" = None):
53
+ self._parent = parent
54
+ if parent is None:
55
+ self._chain = ChainMap({})
56
+ else:
57
+ # New child map on top of parent's chain
58
+ self._chain = parent._chain.new_child()
59
+
60
+ def create_branch(self) -> "ChainMapIsolator":
61
+ """Create a child branch."""
62
+ return ChainMapIsolator(parent=self)
63
+
64
+ def get(self, key: str, default: Any = None) -> Any:
65
+ """Get value, falling through to parent if not in local."""
66
+ return self._chain.get(key, default)
67
+
68
+ def set(self, key: str, value: Any) -> None:
69
+ """Set value in local scope."""
70
+ self._chain.maps[0][key] = value
71
+
72
+ def get_local_changes(self) -> dict[str, Any]:
73
+ """Get only the changes made in this scope."""
74
+ return dict(self._chain.maps[0])
75
+
76
+ def merge_to_parent(self) -> None:
77
+ """Merge local changes to parent."""
78
+ if self._parent is not None:
79
+ local = self.get_local_changes()
80
+ for k, v in local.items():
81
+ self._parent.set(k, v)
82
+
83
+ def __getitem__(self, key: str) -> Any:
84
+ return self._chain[key]
85
+
86
+ def __setitem__(self, key: str, value: Any) -> None:
87
+ self.set(key, value)
88
+
89
+ def __contains__(self, key: str) -> bool:
90
+ return key in self._chain
91
+
92
+ def to_dict(self) -> dict[str, Any]:
93
+ """Flatten to dict (for serialization)."""
94
+ return dict(self._chain)
95
+
96
+
97
+ __all__ = ["StateIsolator", "ChainMapIsolator"]
@@ -0,0 +1,95 @@
1
+ """Centralized logging for aury-agent.
2
+
3
+ Uses standard logging. In applications using foundation-kit,
4
+ loguru will intercept these logs and add trace_id automatically.
5
+
6
+ Usage:
7
+ from aury.agents.core.logging import get_logger
8
+
9
+ logger = get_logger("react") # -> "aury.agents.react"
10
+ logger.info("Starting agent", extra={"session_id": "..."})
11
+
12
+ Pre-defined loggers:
13
+ - root: aury.agents (framework root)
14
+ - react: aury.agents.react
15
+ - workflow: aury.agents.workflow
16
+ - memory: aury.agents.memory
17
+ - tool: aury.agents.tool
18
+ - middleware: aury.agents.middleware
19
+ - bus: aury.agents.bus
20
+ - storage: aury.agents.storage
21
+ - session: aury.agents.session
22
+ """
23
+ import logging
24
+ from typing import Literal
25
+
26
+ # Root namespace
27
+ ROOT_NAMESPACE = "aury.agents"
28
+
29
+ # Pre-defined sub-loggers
30
+ _LOGGERS: dict[str, logging.Logger] = {}
31
+
32
+
33
+ def get_logger(name: str | None = None) -> logging.Logger:
34
+ """Get a logger for the given component.
35
+
36
+ Args:
37
+ name: Component name (e.g., "react", "workflow").
38
+ If None, returns root logger.
39
+
40
+ Returns:
41
+ Logger instance for aury.agents.{name}
42
+ """
43
+ if name is None:
44
+ full_name = ROOT_NAMESPACE
45
+ else:
46
+ full_name = f"{ROOT_NAMESPACE}.{name}"
47
+
48
+ if full_name not in _LOGGERS:
49
+ _LOGGERS[full_name] = logging.getLogger(full_name)
50
+
51
+ return _LOGGERS[full_name]
52
+
53
+
54
+ # Pre-instantiated loggers for common components
55
+ logger = get_logger() # Root logger
56
+ react_logger = get_logger("react")
57
+ workflow_logger = get_logger("workflow")
58
+ memory_logger = get_logger("memory")
59
+ tool_logger = get_logger("tool")
60
+ middleware_logger = get_logger("middleware")
61
+ bus_logger = get_logger("bus")
62
+ storage_logger = get_logger("storage")
63
+ session_logger = get_logger("session")
64
+ context_logger = get_logger("context")
65
+
66
+
67
+ # Convenience type for IDE completion
68
+ LoggerName = Literal[
69
+ "react",
70
+ "workflow",
71
+ "memory",
72
+ "tool",
73
+ "middleware",
74
+ "bus",
75
+ "storage",
76
+ "session",
77
+ "context",
78
+ ]
79
+
80
+
81
+ __all__ = [
82
+ "get_logger",
83
+ "logger",
84
+ "react_logger",
85
+ "workflow_logger",
86
+ "memory_logger",
87
+ "tool_logger",
88
+ "middleware_logger",
89
+ "bus_logger",
90
+ "storage_logger",
91
+ "session_logger",
92
+ "context_logger",
93
+ "LoggerName",
94
+ "ROOT_NAMESPACE",
95
+ ]
@@ -0,0 +1,194 @@
1
+ """Parallel execution utilities for SubAgent.
2
+
3
+ Provides utilities for parallel SubAgent execution, inspired by Google ADK.
4
+ Supports merging async generators from multiple agents.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ from typing import Any, AsyncGenerator, TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from .types.block import BlockEvent
13
+
14
+
15
+ async def merge_agent_runs(
16
+ agent_runs: list[AsyncGenerator["BlockEvent", None]],
17
+ ) -> AsyncGenerator["BlockEvent", None]:
18
+ """Merge multiple agent runs into a single stream.
19
+
20
+ Executes agents in parallel, yielding events as they arrive.
21
+ Each agent's events are processed sequentially to maintain order.
22
+
23
+ Args:
24
+ agent_runs: List of async generators yielding BlockEvents
25
+
26
+ Yields:
27
+ BlockEvent from any of the running agents
28
+
29
+ Example:
30
+ async def run_agent(agent, input):
31
+ async for event in agent.run(input):
32
+ yield event
33
+
34
+ runs = [run_agent(a, "task") for a in agents]
35
+ async for event in merge_agent_runs(runs):
36
+ print(event)
37
+ """
38
+ if not agent_runs:
39
+ return
40
+
41
+ sentinel = object()
42
+ queue: asyncio.Queue[tuple[Any, asyncio.Event | None]] = asyncio.Queue()
43
+
44
+ async def process_agent(events: AsyncGenerator["BlockEvent", None]) -> None:
45
+ """Process single agent's events."""
46
+ try:
47
+ async for event in events:
48
+ # Create resume signal to wait for consumer
49
+ resume_signal = asyncio.Event()
50
+ await queue.put((event, resume_signal))
51
+ # Wait for upstream to consume before generating more
52
+ await resume_signal.wait()
53
+ finally:
54
+ # Mark this agent as finished
55
+ await queue.put((sentinel, None))
56
+
57
+ # Use TaskGroup for parallel execution (Python 3.11+)
58
+ async with asyncio.TaskGroup() as tg:
59
+ for events in agent_runs:
60
+ tg.create_task(process_agent(events))
61
+
62
+ sentinel_count = 0
63
+ # Run until all agents finished
64
+ while sentinel_count < len(agent_runs):
65
+ item, resume_signal = await queue.get()
66
+
67
+ if item is sentinel:
68
+ sentinel_count += 1
69
+ else:
70
+ yield item
71
+ # Signal agent to continue
72
+ if resume_signal:
73
+ resume_signal.set()
74
+
75
+
76
+ async def run_agents_parallel(
77
+ agent_tasks: list[tuple[str, Any]], # [(agent_name, input), ...]
78
+ agent_runner: Any, # Callable to run agent: (name, input) -> AsyncGenerator
79
+ timeout: float | None = None,
80
+ ) -> dict[str, Any]:
81
+ """Run multiple agents in parallel and collect results.
82
+
83
+ Unlike merge_agent_runs which streams events, this collects final results.
84
+
85
+ Args:
86
+ agent_tasks: List of (agent_name, input) tuples
87
+ agent_runner: Async function to run agent
88
+ timeout: Optional timeout in seconds
89
+
90
+ Returns:
91
+ Dict mapping agent_name to result or error
92
+
93
+ Example:
94
+ async def run_agent(name, input):
95
+ agent = get_agent(name)
96
+ result = []
97
+ async for event in agent.run(input):
98
+ if event.kind == BlockKind.TEXT:
99
+ result.append(event.data.get("content", ""))
100
+ return "".join(result)
101
+
102
+ results = await run_agents_parallel(
103
+ [("researcher", "find data"), ("analyzer", "analyze")],
104
+ run_agent,
105
+ timeout=60.0,
106
+ )
107
+ """
108
+ results: dict[str, Any] = {}
109
+
110
+ async def run_one(name: str, input: Any) -> tuple[str, Any]:
111
+ try:
112
+ result = await agent_runner(name, input)
113
+ return (name, result)
114
+ except Exception as e:
115
+ return (name, {"error": str(e)})
116
+
117
+ tasks = [run_one(name, input) for name, input in agent_tasks]
118
+
119
+ if timeout:
120
+ try:
121
+ completed = await asyncio.wait_for(
122
+ asyncio.gather(*tasks, return_exceptions=True),
123
+ timeout=timeout,
124
+ )
125
+ except asyncio.TimeoutError:
126
+ # Return partial results with timeout error for incomplete
127
+ for name, _ in agent_tasks:
128
+ if name not in results:
129
+ results[name] = {"error": "timeout"}
130
+ return results
131
+ else:
132
+ completed = await asyncio.gather(*tasks, return_exceptions=True)
133
+
134
+ for item in completed:
135
+ if isinstance(item, tuple):
136
+ name, result = item
137
+ results[name] = result
138
+ elif isinstance(item, Exception):
139
+ # Exception from gather
140
+ pass
141
+
142
+ return results
143
+
144
+
145
+ class ParallelSubAgentContext:
146
+ """Context for tracking parallel SubAgent execution.
147
+
148
+ Manages branch isolation and result collection for parallel execution.
149
+ """
150
+
151
+ def __init__(
152
+ self,
153
+ parent_invocation_id: str,
154
+ session_id: str,
155
+ ):
156
+ self.parent_invocation_id = parent_invocation_id
157
+ self.session_id = session_id
158
+ self.branches: dict[str, str] = {} # agent_name -> branch_id
159
+ self.results: dict[str, Any] = {}
160
+ self.errors: dict[str, str] = {}
161
+ self._completed: set[str] = set()
162
+
163
+ def create_branch(self, agent_name: str) -> str:
164
+ """Create isolated branch for sub-agent."""
165
+ branch_id = f"{self.parent_invocation_id}.{agent_name}"
166
+ self.branches[agent_name] = branch_id
167
+ return branch_id
168
+
169
+ def mark_completed(self, agent_name: str, result: Any) -> None:
170
+ """Mark agent as completed with result."""
171
+ self._completed.add(agent_name)
172
+ self.results[agent_name] = result
173
+
174
+ def mark_failed(self, agent_name: str, error: str) -> None:
175
+ """Mark agent as failed with error."""
176
+ self._completed.add(agent_name)
177
+ self.errors[agent_name] = error
178
+
179
+ @property
180
+ def all_completed(self) -> bool:
181
+ """Check if all agents completed."""
182
+ return len(self._completed) == len(self.branches)
183
+
184
+ @property
185
+ def pending_agents(self) -> list[str]:
186
+ """Get list of pending agent names."""
187
+ return [n for n in self.branches if n not in self._completed]
188
+
189
+
190
+ __all__ = [
191
+ "merge_agent_runs",
192
+ "run_agents_parallel",
193
+ "ParallelSubAgentContext",
194
+ ]
@@ -0,0 +1,139 @@
1
+ """Runner - Entry point coordinator for agent execution.
2
+
3
+ The Runner handles:
4
+ - Creating InvocationContext from Session/Invocation
5
+ - Managing the execution lifecycle
6
+ - Coordinating services (Session, Message, Storage)
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime
11
+ from typing import Any, AsyncIterator, TYPE_CHECKING
12
+
13
+ from .context import InvocationContext
14
+ from .logging import logger
15
+
16
+ if TYPE_CHECKING:
17
+ from .base import BaseAgent
18
+ from .types.block import BlockEvent
19
+ from .services.session import SessionService
20
+ from .services.message import MessageService
21
+ from ..backends import Backends
22
+ from .event_bus import Bus
23
+ from ..types import PromptInput
24
+
25
+
26
+ class Runner:
27
+ """Entry point for agent execution.
28
+
29
+ Coordinates the execution lifecycle:
30
+ 1. Get or create session
31
+ 2. Create invocation
32
+ 3. Build InvocationContext
33
+ 4. Execute agent
34
+ 5. Handle completion/errors
35
+
36
+ Example:
37
+ runner = Runner(
38
+ session_service=session_service,
39
+ message_service=message_service,
40
+ backends=backends,
41
+ bus=bus,
42
+ )
43
+
44
+ async for response in runner.run(
45
+ agent_class=MyAgent,
46
+ session_id="sess_123",
47
+ input=PromptInput(text="Hello"),
48
+ ):
49
+ print(response)
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ session_service: "SessionService",
55
+ message_service: "MessageService",
56
+ backends: "Backends",
57
+ bus: "Bus",
58
+ ):
59
+ self.session_service = session_service
60
+ self.message_service = message_service
61
+ self.backends = backends
62
+ self.bus = bus
63
+
64
+ async def run(
65
+ self,
66
+ agent_class: type["BaseAgent"],
67
+ session_id: str | None = None,
68
+ input: "PromptInput | None" = None,
69
+ **agent_kwargs: Any,
70
+ ) -> AsyncIterator["BlockEvent"]:
71
+ """Run an agent.
72
+
73
+ Args:
74
+ agent_class: Agent class to instantiate
75
+ session_id: Existing session ID (creates new if None)
76
+ input: Input to agent
77
+ **agent_kwargs: Additional args for agent constructor
78
+
79
+ Yields:
80
+ BlockEvent streaming events
81
+ """
82
+ from ..types import generate_id, Invocation, InvocationState
83
+
84
+ # Get or create session
85
+ if session_id:
86
+ session = await self.session_service.get(session_id)
87
+ if session is None:
88
+ raise ValueError(f"Session not found: {session_id}")
89
+ else:
90
+ agent_name = getattr(agent_class, 'name', agent_class.__name__)
91
+ session = await self.session_service.create(root_agent_id=agent_name)
92
+
93
+ logger.info(f"Starting agent execution: session={session.id}")
94
+
95
+ # Create invocation
96
+ invocation = Invocation(
97
+ id=generate_id("inv"),
98
+ session_id=session.id,
99
+ agent_id=getattr(agent_class, 'name', agent_class.__name__),
100
+ state=InvocationState.RUNNING,
101
+ started_at=datetime.now(),
102
+ )
103
+
104
+ # Build context
105
+ ctx = InvocationContext(
106
+ session_id=session.id,
107
+ invocation_id=invocation.id,
108
+ agent_id=invocation.agent_id,
109
+ backends=self.backends,
110
+ bus=self.bus,
111
+ )
112
+
113
+ # Create agent instance
114
+ agent = agent_class(
115
+ ctx=ctx,
116
+ **agent_kwargs,
117
+ )
118
+
119
+ try:
120
+ # Execute
121
+ async for response in agent.run(input):
122
+ yield response
123
+
124
+ invocation.state = InvocationState.COMPLETED
125
+ logger.info(f"Agent completed: invocation={invocation.id}")
126
+
127
+ except Exception as e:
128
+ invocation.state = InvocationState.FAILED
129
+ logger.error(f"Agent failed: invocation={invocation.id}, error={e}")
130
+ raise
131
+
132
+ finally:
133
+ invocation.finished_at = datetime.now()
134
+ # Persist invocation
135
+ if self.backends.state:
136
+ await self.backends.state.set("invocation", invocation.id, invocation.__dict__)
137
+
138
+
139
+ __all__ = ["Runner"]
@@ -0,0 +1,5 @@
1
+ """Service protocols and implementations."""
2
+ from .session import SessionService
3
+ from .message import MessageService
4
+
5
+ __all__ = ["SessionService", "MessageService"]
@@ -0,0 +1,144 @@
1
+ """File-based session service implementation."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..logging import session_logger as logger
9
+ from ..types.session import Session, ControlFrame
10
+
11
+
12
+ class FileSessionService:
13
+ """File-based session storage.
14
+
15
+ Stores sessions as JSON files in a directory.
16
+ Suitable for development and single-instance deployments.
17
+
18
+ Directory structure:
19
+ {base_dir}/
20
+ {session_id}.json
21
+ """
22
+
23
+ def __init__(self, base_dir: str | Path):
24
+ """Initialize file session service.
25
+
26
+ Args:
27
+ base_dir: Directory to store session files
28
+ """
29
+ self.base_dir = Path(base_dir)
30
+ self.base_dir.mkdir(parents=True, exist_ok=True)
31
+
32
+ def _session_path(self, session_id: str) -> Path:
33
+ """Get file path for session."""
34
+ # Sanitize ID to prevent path traversal
35
+ safe_id = "".join(c for c in session_id if c.isalnum() or c in "-_")
36
+ return self.base_dir / f"{safe_id}.json"
37
+
38
+ async def create(self, root_agent_id: str, **kwargs) -> Session:
39
+ """Create a new session."""
40
+ session = Session(root_agent_id=root_agent_id, **kwargs)
41
+ await self._save(session)
42
+ logger.debug(f"Created session: {session.id}")
43
+ return session
44
+
45
+ async def get(self, session_id: str) -> Session | None:
46
+ """Get session by ID."""
47
+ path = self._session_path(session_id)
48
+ if not path.exists():
49
+ return None
50
+
51
+ try:
52
+ data = json.loads(path.read_text(encoding="utf-8"))
53
+ return Session.from_dict(data)
54
+ except Exception as e:
55
+ logger.error(f"Failed to load session {session_id}: {e}")
56
+ return None
57
+
58
+ async def update(self, session: Session) -> None:
59
+ """Update session."""
60
+ await self._save(session)
61
+ logger.debug(f"Updated session: {session.id}")
62
+
63
+ async def delete(self, session_id: str) -> bool:
64
+ """Delete session."""
65
+ path = self._session_path(session_id)
66
+ if path.exists():
67
+ try:
68
+ path.unlink()
69
+ logger.debug(f"Deleted session: {session_id}")
70
+ return True
71
+ except Exception as e:
72
+ logger.error(f"Failed to delete session {session_id}: {e}")
73
+ return False
74
+
75
+ async def list(self, limit: int = 100, offset: int = 0) -> list[Session]:
76
+ """List sessions, ordered by updated_at descending."""
77
+ sessions = []
78
+
79
+ for path in self.base_dir.glob("*.json"):
80
+ try:
81
+ data = json.loads(path.read_text(encoding="utf-8"))
82
+ sessions.append(Session.from_dict(data))
83
+ except Exception as e:
84
+ logger.warning(f"Failed to load session file {path}: {e}")
85
+ continue
86
+
87
+ # Sort by updated_at descending
88
+ sessions.sort(key=lambda s: s.updated_at, reverse=True)
89
+
90
+ # Apply pagination
91
+ return sessions[offset : offset + limit]
92
+
93
+ async def push_control(self, session_id: str, frame: ControlFrame) -> None:
94
+ """Push control frame to session's control stack."""
95
+ session = await self.get(session_id)
96
+ if session is None:
97
+ raise ValueError(f"Session not found: {session_id}")
98
+
99
+ session.push_control(frame)
100
+ await self._save(session)
101
+ logger.debug(f"Pushed control frame to session {session_id}: {frame.agent_id}")
102
+
103
+ async def pop_control(self, session_id: str) -> ControlFrame | None:
104
+ """Pop control frame from session's control stack."""
105
+ session = await self.get(session_id)
106
+ if session is None:
107
+ raise ValueError(f"Session not found: {session_id}")
108
+
109
+ frame = session.pop_control()
110
+ if frame:
111
+ await self._save(session)
112
+ logger.debug(f"Popped control frame from session {session_id}: {frame.agent_id}")
113
+ return frame
114
+
115
+ async def _save(self, session: Session) -> None:
116
+ """Save session to file."""
117
+ path = self._session_path(session.id)
118
+ try:
119
+ path.write_text(
120
+ json.dumps(session.to_dict(), indent=2, ensure_ascii=False),
121
+ encoding="utf-8"
122
+ )
123
+ except Exception as e:
124
+ logger.error(f"Failed to save session {session.id}: {e}")
125
+ raise
126
+
127
+ async def get_by_root_agent(self, root_agent_id: str, limit: int = 100) -> list[Session]:
128
+ """Get sessions by root agent ID."""
129
+ all_sessions = await self.list(limit=10000) # Get all
130
+ return [
131
+ s for s in all_sessions
132
+ if s.root_agent_id == root_agent_id
133
+ ][:limit]
134
+
135
+ async def get_active(self, limit: int = 100) -> list[Session]:
136
+ """Get active sessions."""
137
+ all_sessions = await self.list(limit=10000)
138
+ return [
139
+ s for s in all_sessions
140
+ if s.is_active
141
+ ][:limit]
142
+
143
+
144
+ __all__ = ["FileSessionService"]