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,136 @@
1
+ """Expression evaluator for workflow DSL."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from typing import Any
6
+
7
+
8
+ class ExpressionError(Exception):
9
+ """Expression evaluation error."""
10
+ pass
11
+
12
+
13
+ class DotDict(dict):
14
+ """Dict that supports dot notation access.
15
+
16
+ Allows expressions like `inputs.json_data` instead of `inputs["json_data"]`.
17
+ """
18
+
19
+ def __getattr__(self, key: str) -> Any:
20
+ try:
21
+ value = self[key]
22
+ # Recursively wrap nested dicts
23
+ if isinstance(value, dict) and not isinstance(value, DotDict):
24
+ return DotDict(value)
25
+ return value
26
+ except KeyError:
27
+ raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
28
+
29
+ def __setattr__(self, key: str, value: Any) -> None:
30
+ self[key] = value
31
+
32
+ def __delattr__(self, key: str) -> None:
33
+ try:
34
+ del self[key]
35
+ except KeyError:
36
+ raise AttributeError(f"'{type(self).__name__}' has no attribute '{key}'")
37
+
38
+
39
+ class ExpressionEvaluator:
40
+ """Expression evaluator for ${{ ... }} syntax."""
41
+
42
+ EXPR_PATTERN = re.compile(r'\$\{\{\s*(.+?)\s*\}\}')
43
+
44
+ def evaluate(
45
+ self,
46
+ expression: str,
47
+ context: dict[str, Any],
48
+ ) -> Any:
49
+ """Evaluate expression.
50
+
51
+ Supports:
52
+ - Full expressions: ${{ inputs.name }}
53
+ - Template strings: "Hello ${{ inputs.name }}!"
54
+ - Comparisons: ${{ inputs.count > 10 }}
55
+ """
56
+ if expression.startswith("${{") and expression.endswith("}}"):
57
+ inner = expression[3:-2].strip()
58
+ return self._eval_inner(inner, context)
59
+
60
+ def replace(match: re.Match) -> str:
61
+ inner = match.group(1)
62
+ result = self._eval_inner(inner, context)
63
+ return str(result) if result is not None else ""
64
+
65
+ return self.EXPR_PATTERN.sub(replace, expression)
66
+
67
+ def _eval_inner(self, expr: str, context: dict[str, Any]) -> Any:
68
+ """Evaluate inner expression.
69
+
70
+ Uses DotDict to allow dot notation access like `inputs.json_data`.
71
+ """
72
+ # Wrap dicts in DotDict to support dot notation
73
+ inputs_data = context.get("inputs", {})
74
+ state_data = context.get("state", {})
75
+
76
+ safe_context = {
77
+ "inputs": DotDict(inputs_data) if isinstance(inputs_data, dict) else inputs_data,
78
+ "state": DotDict(state_data.to_dict() if hasattr(state_data, 'to_dict') else state_data) if state_data else DotDict({}),
79
+ "true": True,
80
+ "false": False,
81
+ "null": None,
82
+ "True": True,
83
+ "False": False,
84
+ "None": None,
85
+ }
86
+
87
+ # Add extra context variables (also wrap dicts)
88
+ for key, value in context.items():
89
+ if key not in ("inputs", "state"):
90
+ if isinstance(value, dict):
91
+ safe_context[key] = DotDict(value)
92
+ else:
93
+ safe_context[key] = value
94
+
95
+ try:
96
+ return eval(expr, {"__builtins__": {}}, safe_context)
97
+ except Exception as e:
98
+ raise ExpressionError(f"Failed to evaluate: {expr}") from e
99
+
100
+ def evaluate_condition(self, expr: str | None, context: dict[str, Any]) -> bool:
101
+ """Evaluate condition expression."""
102
+ if not expr:
103
+ return True
104
+ result = self.evaluate(expr, context)
105
+ return bool(result)
106
+
107
+ def has_expression(self, value: str) -> bool:
108
+ """Check if string contains expression."""
109
+ if not isinstance(value, str):
110
+ return False
111
+ return bool(self.EXPR_PATTERN.search(value))
112
+
113
+ def resolve_inputs(
114
+ self,
115
+ inputs: dict[str, Any],
116
+ context: dict[str, Any],
117
+ ) -> dict[str, Any]:
118
+ """Resolve all expressions in inputs dict."""
119
+ resolved = {}
120
+
121
+ for key, value in inputs.items():
122
+ if isinstance(value, str) and self.has_expression(value):
123
+ resolved[key] = self.evaluate(value, context)
124
+ elif isinstance(value, dict):
125
+ resolved[key] = self.resolve_inputs(value, context)
126
+ elif isinstance(value, list):
127
+ resolved[key] = [
128
+ self.evaluate(v, context)
129
+ if isinstance(v, str) and self.has_expression(v)
130
+ else v
131
+ for v in value
132
+ ]
133
+ else:
134
+ resolved[key] = value
135
+
136
+ return resolved
@@ -0,0 +1,182 @@
1
+ """Workflow DSL parser."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+
9
+ from .types import (
10
+ NodeType,
11
+ Position,
12
+ NodeSpec,
13
+ EdgeSpec,
14
+ WorkflowSpec,
15
+ Workflow,
16
+ )
17
+
18
+
19
+ class WorkflowValidationError(Exception):
20
+ """Workflow validation error."""
21
+
22
+ def __init__(self, errors: list[str]):
23
+ self.errors = errors
24
+ super().__init__(f"Workflow validation failed: {'; '.join(errors)}")
25
+
26
+
27
+ class WorkflowParser:
28
+ """Workflow DSL parser."""
29
+
30
+ def parse(self, source: str | Path) -> Workflow:
31
+ """Parse YAML file or string."""
32
+ raw = self._load_yaml(source)
33
+ spec = self._parse_spec(raw)
34
+
35
+ errors = self.validate(spec)
36
+ if errors:
37
+ raise WorkflowValidationError(errors)
38
+
39
+ return self._build_workflow(spec)
40
+
41
+ def _load_yaml(self, source: str | Path) -> dict[str, Any]:
42
+ """Load YAML from file or string."""
43
+ if isinstance(source, Path):
44
+ with open(source, 'r') as f:
45
+ return yaml.safe_load(f)
46
+ elif isinstance(source, str):
47
+ if source.endswith('.yaml') or source.endswith('.yml'):
48
+ with open(source, 'r') as f:
49
+ return yaml.safe_load(f)
50
+ else:
51
+ return yaml.safe_load(source)
52
+ raise ValueError(f"Invalid source type: {type(source)}")
53
+
54
+ def _parse_spec(self, raw: dict[str, Any]) -> WorkflowSpec:
55
+ """Parse raw dict to WorkflowSpec."""
56
+ nodes = []
57
+ for n in raw.get("nodes", []):
58
+ pos_data = n.get("position", {"x": 0, "y": 0})
59
+ position = Position(x=pos_data.get("x", 0), y=pos_data.get("y", 0))
60
+
61
+ node = NodeSpec(
62
+ id=n["id"],
63
+ type=NodeType(n["type"]),
64
+ position=position,
65
+ agent=n.get("agent"),
66
+ config=n.get("config", {}),
67
+ inputs=n.get("inputs", {}),
68
+ output=n.get("output"),
69
+ when=n.get("when"),
70
+ branches=n.get("branches"),
71
+ steps=n.get("steps"),
72
+ expression=n.get("expression"),
73
+ then_node=n.get("then"),
74
+ else_node=n.get("else"),
75
+ )
76
+ nodes.append(node)
77
+
78
+ edges = []
79
+ for e in raw.get("edges", []):
80
+ edge = EdgeSpec(
81
+ from_node=e["from"],
82
+ to_node=e["to"],
83
+ when=e.get("when"),
84
+ )
85
+ edges.append(edge)
86
+
87
+ return WorkflowSpec(
88
+ name=raw["name"],
89
+ version=raw.get("version", "1.0"),
90
+ description=raw.get("description"),
91
+ state=raw.get("state", {}),
92
+ inputs=raw.get("inputs", {}),
93
+ nodes=nodes,
94
+ edges=edges,
95
+ )
96
+
97
+ def validate(self, spec: WorkflowSpec) -> list[str]:
98
+ """Validate workflow spec."""
99
+ errors = []
100
+ node_ids = {n.id for n in spec.nodes}
101
+
102
+ # Check edge references
103
+ for edge in spec.edges:
104
+ if edge.from_node not in node_ids:
105
+ errors.append(f"Edge references unknown node: {edge.from_node}")
106
+ if edge.to_node not in node_ids:
107
+ errors.append(f"Edge references unknown node: {edge.to_node}")
108
+
109
+ # Check for trigger node
110
+ triggers = [n for n in spec.nodes if n.type == NodeType.TRIGGER]
111
+ if not triggers:
112
+ errors.append("Workflow must have at least one trigger node")
113
+
114
+ # Check for cycles
115
+ if self._has_cycle(spec):
116
+ errors.append("Workflow contains cycles")
117
+
118
+ # Check agent nodes
119
+ for node in spec.nodes:
120
+ if node.type == NodeType.AGENT and not node.agent:
121
+ errors.append(f"Agent node '{node.id}' must specify 'agent' type")
122
+
123
+ # Check condition nodes
124
+ for node in spec.nodes:
125
+ if node.type == NodeType.CONDITION:
126
+ if not node.expression:
127
+ errors.append(f"Condition node '{node.id}' must have 'expression'")
128
+ if not node.then_node:
129
+ errors.append(f"Condition node '{node.id}' must have 'then'")
130
+
131
+ return errors
132
+
133
+ def _has_cycle(self, spec: WorkflowSpec) -> bool:
134
+ """Detect cycles using DFS."""
135
+ visited: set[str] = set()
136
+ rec_stack: set[str] = set()
137
+
138
+ adj: dict[str, list[str]] = {n.id: [] for n in spec.nodes}
139
+ for edge in spec.edges:
140
+ adj[edge.from_node].append(edge.to_node)
141
+
142
+ def dfs(node_id: str) -> bool:
143
+ visited.add(node_id)
144
+ rec_stack.add(node_id)
145
+
146
+ for neighbor in adj.get(node_id, []):
147
+ if neighbor not in visited:
148
+ if dfs(neighbor):
149
+ return True
150
+ elif neighbor in rec_stack:
151
+ return True
152
+
153
+ rec_stack.remove(node_id)
154
+ return False
155
+
156
+ for node in spec.nodes:
157
+ if node.id not in visited:
158
+ if dfs(node.id):
159
+ return True
160
+
161
+ return False
162
+
163
+ def _build_workflow(self, spec: WorkflowSpec) -> Workflow:
164
+ """Build executable workflow."""
165
+ node_map = {n.id: n for n in spec.nodes}
166
+
167
+ incoming_edges: dict[str, list[str]] = {n.id: [] for n in spec.nodes}
168
+ outgoing_edges: dict[str, list[str]] = {n.id: [] for n in spec.nodes}
169
+ edge_conditions: dict[tuple[str, str], str | None] = {}
170
+
171
+ for edge in spec.edges:
172
+ incoming_edges[edge.to_node].append(edge.from_node)
173
+ outgoing_edges[edge.from_node].append(edge.to_node)
174
+ edge_conditions[(edge.from_node, edge.to_node)] = edge.when
175
+
176
+ return Workflow(
177
+ spec=spec,
178
+ node_map=node_map,
179
+ incoming_edges=incoming_edges,
180
+ outgoing_edges=outgoing_edges,
181
+ edge_conditions=edge_conditions,
182
+ )
@@ -0,0 +1,145 @@
1
+ """Workflow state management with branch isolation."""
2
+ from __future__ import annotations
3
+
4
+ from collections import ChainMap
5
+ from typing import Any, Callable, Iterator, Protocol
6
+
7
+
8
+ class MergeStrategy(Protocol):
9
+ """Merge strategy protocol."""
10
+
11
+ def merge(self, results: list[Any]) -> Any:
12
+ """Merge parallel results."""
13
+ ...
14
+
15
+
16
+ class CollectListStrategy:
17
+ """Collect results as list."""
18
+
19
+ def merge(self, results: list[Any]) -> list[Any]:
20
+ return [r for r in results if r is not None]
21
+
22
+
23
+ class CollectDictStrategy:
24
+ """Collect results as dict."""
25
+
26
+ def __init__(self, key_fn: Callable[[Any], str] | None = None):
27
+ self.key_fn = key_fn or (lambda x: str(id(x)))
28
+
29
+ def merge(self, results: list[Any]) -> dict[str, Any]:
30
+ return {self.key_fn(r): r for r in results if r is not None}
31
+
32
+
33
+ class FirstSuccessStrategy:
34
+ """Take first non-None result."""
35
+
36
+ def merge(self, results: list[Any]) -> Any:
37
+ return next((r for r in results if r is not None), None)
38
+
39
+
40
+ class CustomMergeStrategy:
41
+ """Custom merge function."""
42
+
43
+ def __init__(self, merge_fn: Callable[[list[Any]], Any]):
44
+ self.merge_fn = merge_fn
45
+
46
+ def merge(self, results: list[Any]) -> Any:
47
+ return self.merge_fn(results)
48
+
49
+
50
+ class WorkflowState:
51
+ """Workflow state with branch isolation.
52
+
53
+ Uses ChainMap for copy-on-write semantics.
54
+ """
55
+
56
+ def __init__(self, parent: WorkflowState | None = None):
57
+ self._local: dict[str, Any] = {}
58
+ self._parent = parent
59
+
60
+ if parent:
61
+ self._chain: ChainMap[str, Any] = ChainMap(self._local, parent._chain)
62
+ else:
63
+ self._chain = ChainMap(self._local)
64
+
65
+ def __getitem__(self, key: str) -> Any:
66
+ return self._chain[key]
67
+
68
+ def __setitem__(self, key: str, value: Any) -> None:
69
+ self._local[key] = value
70
+
71
+ def __contains__(self, key: str) -> bool:
72
+ return key in self._chain
73
+
74
+ def __iter__(self) -> Iterator[str]:
75
+ return iter(self._chain)
76
+
77
+ def get(self, key: str, default: Any = None) -> Any:
78
+ return self._chain.get(key, default)
79
+
80
+ def keys(self) -> Any:
81
+ return self._chain.keys()
82
+
83
+ def values(self) -> Any:
84
+ return self._chain.values()
85
+
86
+ def items(self) -> Any:
87
+ return self._chain.items()
88
+
89
+ def create_branch(self) -> WorkflowState:
90
+ """Create branch for parallel execution."""
91
+ return WorkflowState(parent=self)
92
+
93
+ def get_local_changes(self) -> dict[str, Any]:
94
+ """Get local changes."""
95
+ return dict(self._local)
96
+
97
+ def merge_from(
98
+ self,
99
+ other: WorkflowState,
100
+ strategy: str = "overwrite",
101
+ ) -> None:
102
+ """Merge changes from another state."""
103
+ changes = other.get_local_changes()
104
+
105
+ for key, value in changes.items():
106
+ if strategy == "overwrite":
107
+ self._local[key] = value
108
+ elif strategy == "append":
109
+ if key in self._local:
110
+ existing = self._local[key]
111
+ if isinstance(existing, list):
112
+ existing.append(value)
113
+ else:
114
+ self._local[key] = [existing, value]
115
+ else:
116
+ self._local[key] = [value]
117
+
118
+ def to_dict(self) -> dict[str, Any]:
119
+ """Convert to regular dict."""
120
+ return dict(self._chain)
121
+
122
+ @classmethod
123
+ def from_dict(cls, data: dict[str, Any]) -> "WorkflowState":
124
+ """Create from dict."""
125
+ state = cls()
126
+ state._local.update(data)
127
+ return state
128
+
129
+ def clear(self) -> None:
130
+ """Clear local state."""
131
+ self._local.clear()
132
+
133
+
134
+ def get_merge_strategy(strategy: str, **kwargs: Any) -> MergeStrategy:
135
+ """Get merge strategy by name."""
136
+ match strategy:
137
+ case "collect_list":
138
+ return CollectListStrategy()
139
+ case "collect_dict":
140
+ key_fn = kwargs.get("key_fn")
141
+ return CollectDictStrategy(key_fn=key_fn)
142
+ case "first_success":
143
+ return FirstSuccessStrategy()
144
+ case _:
145
+ return CollectListStrategy()
@@ -0,0 +1,86 @@
1
+ """Workflow DSL types."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from typing import Any, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..middleware import Middleware, MiddlewareChain
10
+
11
+
12
+ class NodeType(Enum):
13
+ """Workflow node types."""
14
+ TRIGGER = "trigger"
15
+ AGENT = "agent"
16
+ PARALLEL = "parallel"
17
+ SEQUENCE = "sequence"
18
+ CONDITION = "condition"
19
+ TERMINAL = "terminal"
20
+
21
+
22
+ @dataclass
23
+ class Position:
24
+ """Node position for visual editors."""
25
+ x: float = 0.0
26
+ y: float = 0.0
27
+
28
+
29
+ @dataclass
30
+ class NodeSpec:
31
+ """Node specification."""
32
+ id: str
33
+ type: NodeType
34
+ position: Position = field(default_factory=Position)
35
+ agent: str | None = None
36
+ config: dict[str, Any] = field(default_factory=dict)
37
+ inputs: dict[str, Any] = field(default_factory=dict)
38
+ output: str | None = None
39
+ when: str | None = None # Conditional expression
40
+
41
+ # For parallel/sequence nodes
42
+ branches: list[dict[str, Any]] | None = None
43
+ steps: list[str] | None = None
44
+
45
+ # For condition nodes
46
+ expression: str | None = None
47
+ then_node: str | None = None
48
+ else_node: str | None = None
49
+
50
+ # Node-level middleware (overrides/extends workflow middleware)
51
+ middleware: list["Middleware"] | None = None
52
+
53
+
54
+ @dataclass
55
+ class EdgeSpec:
56
+ """Edge specification."""
57
+ from_node: str
58
+ to_node: str
59
+ when: str | None = None # Conditional edge
60
+
61
+
62
+ @dataclass
63
+ class WorkflowSpec:
64
+ """Workflow specification from DSL."""
65
+ name: str
66
+ version: str = "1.0"
67
+ description: str | None = None
68
+ state: dict[str, str] = field(default_factory=dict)
69
+ inputs: dict[str, dict[str, Any]] = field(default_factory=dict)
70
+ nodes: list[NodeSpec] = field(default_factory=list)
71
+ edges: list[EdgeSpec] = field(default_factory=list)
72
+
73
+ # Workflow-level middleware (applied to all nodes)
74
+ middleware: "MiddlewareChain | None" = None
75
+
76
+
77
+ @dataclass
78
+ class Workflow:
79
+ """Parsed executable workflow."""
80
+ spec: WorkflowSpec
81
+
82
+ # Computed properties
83
+ node_map: dict[str, NodeSpec] = field(default_factory=dict)
84
+ incoming_edges: dict[str, list[str]] = field(default_factory=dict)
85
+ outgoing_edges: dict[str, list[str]] = field(default_factory=dict)
86
+ edge_conditions: dict[tuple[str, str], str | None] = field(default_factory=dict)
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: aury-agent
3
+ Version: 0.0.4
4
+ Summary: Aury Agent Framework - React Agent and Workflow orchestration
5
+ Author: Aury Team
6
+ License: MIT
7
+ Keywords: agent,llm,react,workflow
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Python: >=3.13
14
+ Requires-Dist: aiofiles>=23.0.0
15
+ Requires-Dist: aury-ai-model[all]
16
+ Requires-Dist: pydantic>=2.0.0
17
+ Requires-Dist: python-dotenv>=1.0.0
18
+ Requires-Dist: pyyaml>=6.0.0
19
+ Requires-Dist: rich>=13.0.0
20
+ Requires-Dist: typer>=0.9.0
21
+ Provides-Extra: all
22
+ Requires-Dist: anthropic>=0.20.0; extra == 'all'
23
+ Requires-Dist: openai>=1.0.0; extra == 'all'
24
+ Provides-Extra: anthropic
25
+ Requires-Dist: anthropic>=0.20.0; extra == 'anthropic'
26
+ Provides-Extra: memory
27
+ Requires-Dist: chromadb>=0.4.0; extra == 'memory'
28
+ Provides-Extra: openai
29
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Aury Agents
33
+
34
+ A flexible Agent framework supporting both React Agent (autonomous loop) and Workflow (DAG orchestration) patterns.
35
+
36
+ ## Features
37
+
38
+ - **React Agent**: Autonomous agent with tool calling loop
39
+ - **Workflow**: DAG-based orchestration for complex pipelines
40
+ - **Session Management**: Event-sourced session with snapshot and revert
41
+ - **Permission System**: Fine-grained tool execution control
42
+ - **Memory System**: Semantic memory with vector storage
43
+ - **Compaction**: Token-efficient session history management
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install aury-agent
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```python
54
+ from aury.agents import ReactAgent, Tool
55
+
56
+ # Define a tool
57
+ @tool
58
+ def search(query: str) -> str:
59
+ """Search the web."""
60
+ return f"Results for: {query}"
61
+
62
+ # Create agent
63
+ agent = ReactAgent(
64
+ tools=[search],
65
+ llm_provider=your_llm_provider,
66
+ )
67
+
68
+ # Run
69
+ result = await agent.run("What is the weather today?")
70
+ ```
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ # Install dev dependencies
76
+ uv sync
77
+
78
+ # Run tests
79
+ pytest
80
+
81
+ # Type check
82
+ mypy aury
83
+
84
+ # Lint
85
+ ruff check aury
86
+ ```
87
+
88
+ ## License
89
+
90
+ MIT