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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- 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,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"]
|