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,262 @@
1
+ """SQLite-based state backend for local persistence.
2
+
3
+ Provides persistent storage using SQLite database.
4
+ Ideal for local development and single-user applications.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import sqlite3
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ class SQLiteStateBackend:
15
+ """SQLite-based state backend.
16
+
17
+ Stores all framework state in a single SQLite database file.
18
+ Supports both key-value storage and message history.
19
+
20
+ Tables:
21
+ - state: namespace/key/value for general storage
22
+ - messages: session_id/role/content for conversation history
23
+
24
+ Example:
25
+ backend = SQLiteStateBackend("./data/agent.db")
26
+
27
+ # Key-value storage
28
+ await backend.set("sessions", "sess_123", {"id": "sess_123"})
29
+ data = await backend.get("sessions", "sess_123")
30
+
31
+ # Message history
32
+ await backend.add_message("sess_123", {"role": "user", "content": "Hello"})
33
+ messages = await backend.get_messages("sess_123")
34
+ """
35
+
36
+ def __init__(self, db_path: str | Path = "./data/agent.db"):
37
+ """Initialize SQLite backend.
38
+
39
+ Args:
40
+ db_path: Path to SQLite database file
41
+ """
42
+ self.db_path = Path(db_path)
43
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
44
+ self._init_db()
45
+
46
+ def _init_db(self) -> None:
47
+ """Initialize database tables."""
48
+ conn = sqlite3.connect(self.db_path)
49
+ try:
50
+ conn.execute("""
51
+ CREATE TABLE IF NOT EXISTS state (
52
+ namespace TEXT NOT NULL,
53
+ key TEXT NOT NULL,
54
+ value TEXT NOT NULL,
55
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
56
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
57
+ PRIMARY KEY (namespace, key)
58
+ )
59
+ """)
60
+ conn.execute("""
61
+ CREATE TABLE IF NOT EXISTS messages (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ session_id TEXT NOT NULL,
64
+ namespace TEXT,
65
+ role TEXT NOT NULL,
66
+ content TEXT NOT NULL,
67
+ invocation_id TEXT,
68
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
69
+ )
70
+ """)
71
+ conn.execute("""
72
+ CREATE INDEX IF NOT EXISTS idx_messages_session
73
+ ON messages(session_id, namespace)
74
+ """)
75
+ conn.commit()
76
+ finally:
77
+ conn.close()
78
+
79
+ def _get_conn(self) -> sqlite3.Connection:
80
+ """Get database connection."""
81
+ conn = sqlite3.connect(self.db_path)
82
+ conn.row_factory = sqlite3.Row
83
+ return conn
84
+
85
+ # ========== Key-Value Storage ==========
86
+
87
+ async def get(self, namespace: str, key: str) -> Any | None:
88
+ """Get value by key."""
89
+ conn = self._get_conn()
90
+ try:
91
+ cursor = conn.execute(
92
+ "SELECT value FROM state WHERE namespace = ? AND key = ?",
93
+ (namespace, key)
94
+ )
95
+ row = cursor.fetchone()
96
+ if row:
97
+ return json.loads(row["value"])
98
+ return None
99
+ finally:
100
+ conn.close()
101
+
102
+ async def set(self, namespace: str, key: str, value: Any) -> None:
103
+ """Set value by key."""
104
+ conn = self._get_conn()
105
+ try:
106
+ value_json = json.dumps(value, default=str, ensure_ascii=False)
107
+ conn.execute("""
108
+ INSERT INTO state (namespace, key, value, updated_at)
109
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
110
+ ON CONFLICT(namespace, key) DO UPDATE SET
111
+ value = excluded.value,
112
+ updated_at = CURRENT_TIMESTAMP
113
+ """, (namespace, key, value_json))
114
+ conn.commit()
115
+ finally:
116
+ conn.close()
117
+
118
+ async def delete(self, namespace: str, key: str) -> bool:
119
+ """Delete value by key."""
120
+ conn = self._get_conn()
121
+ try:
122
+ cursor = conn.execute(
123
+ "DELETE FROM state WHERE namespace = ? AND key = ?",
124
+ (namespace, key)
125
+ )
126
+ conn.commit()
127
+ return cursor.rowcount > 0
128
+ finally:
129
+ conn.close()
130
+
131
+ async def list(self, namespace: str, prefix: str = "") -> list[str]:
132
+ """List keys with optional prefix filter."""
133
+ conn = self._get_conn()
134
+ try:
135
+ if prefix:
136
+ cursor = conn.execute(
137
+ "SELECT key FROM state WHERE namespace = ? AND key LIKE ?",
138
+ (namespace, f"{prefix}%")
139
+ )
140
+ else:
141
+ cursor = conn.execute(
142
+ "SELECT key FROM state WHERE namespace = ?",
143
+ (namespace,)
144
+ )
145
+ return [row["key"] for row in cursor.fetchall()]
146
+ finally:
147
+ conn.close()
148
+
149
+ async def exists(self, namespace: str, key: str) -> bool:
150
+ """Check if key exists."""
151
+ conn = self._get_conn()
152
+ try:
153
+ cursor = conn.execute(
154
+ "SELECT 1 FROM state WHERE namespace = ? AND key = ?",
155
+ (namespace, key)
156
+ )
157
+ return cursor.fetchone() is not None
158
+ finally:
159
+ conn.close()
160
+
161
+ # ========== Message Storage ==========
162
+
163
+ async def add_message(
164
+ self,
165
+ session_id: str,
166
+ message: dict[str, Any],
167
+ namespace: str | None = None,
168
+ ) -> None:
169
+ """Add a message to session history.
170
+
171
+ Args:
172
+ session_id: Session ID
173
+ message: Message dict with role, content, invocation_id
174
+ namespace: Optional namespace for isolation
175
+ """
176
+ conn = self._get_conn()
177
+ try:
178
+ content = message.get("content", "")
179
+ # Serialize list content (tool_use etc.)
180
+ if isinstance(content, list):
181
+ content = json.dumps(content, ensure_ascii=False)
182
+
183
+ conn.execute("""
184
+ INSERT INTO messages (session_id, namespace, role, content, invocation_id)
185
+ VALUES (?, ?, ?, ?, ?)
186
+ """, (
187
+ session_id,
188
+ namespace,
189
+ message.get("role", "user"),
190
+ content,
191
+ message.get("invocation_id"),
192
+ ))
193
+ conn.commit()
194
+ finally:
195
+ conn.close()
196
+
197
+ async def get_messages(
198
+ self,
199
+ session_id: str,
200
+ namespace: str | None = None,
201
+ ) -> list[dict[str, Any]]:
202
+ """Get all messages for a session.
203
+
204
+ Args:
205
+ session_id: Session ID
206
+ namespace: Optional namespace filter
207
+
208
+ Returns:
209
+ List of message dicts in chronological order
210
+ """
211
+ conn = self._get_conn()
212
+ try:
213
+ if namespace is None:
214
+ cursor = conn.execute(
215
+ "SELECT role, content, invocation_id FROM messages WHERE session_id = ? AND namespace IS NULL ORDER BY id",
216
+ (session_id,)
217
+ )
218
+ else:
219
+ cursor = conn.execute(
220
+ "SELECT role, content, invocation_id FROM messages WHERE session_id = ? AND namespace = ? ORDER BY id",
221
+ (session_id, namespace)
222
+ )
223
+
224
+ result = []
225
+ for row in cursor.fetchall():
226
+ content = row["content"]
227
+ # Try to parse JSON content (may be tool_use etc.)
228
+ if content and content.startswith("["):
229
+ try:
230
+ content = json.loads(content)
231
+ except json.JSONDecodeError:
232
+ pass
233
+
234
+ result.append({
235
+ "role": row["role"],
236
+ "content": content,
237
+ "invocation_id": row["invocation_id"],
238
+ })
239
+
240
+ return result
241
+ finally:
242
+ conn.close()
243
+
244
+ async def clear_messages(self, session_id: str) -> int:
245
+ """Clear all messages for a session.
246
+
247
+ Returns:
248
+ Number of messages deleted
249
+ """
250
+ conn = self._get_conn()
251
+ try:
252
+ cursor = conn.execute(
253
+ "DELETE FROM messages WHERE session_id = ?",
254
+ (session_id,)
255
+ )
256
+ conn.commit()
257
+ return cursor.rowcount
258
+ finally:
259
+ conn.close()
260
+
261
+
262
+ __all__ = ["SQLiteStateBackend"]
@@ -0,0 +1,178 @@
1
+ """State backend types and protocols.
2
+
3
+ StateBackend is a simplified key-value storage for generic state.
4
+ For specific data types, use dedicated backends:
5
+ - SessionBackend: Session management
6
+ - InvocationBackend: Invocation management
7
+ - MessageBackend: Message storage
8
+ - MemoryBackend: Long-term memory
9
+ - ArtifactBackend: File/artifact storage
10
+
11
+ Architecture:
12
+ - StateStore: Low-level storage protocol (implemented by API layer)
13
+ - StateBackend: High-level interface (used by framework, wraps StateStore)
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Protocol, runtime_checkable
18
+
19
+
20
+ @runtime_checkable
21
+ class StateStore(Protocol):
22
+ """Low-level storage protocol for state persistence.
23
+
24
+ This is the interface that API/application layer implements.
25
+ Provides raw key-value operations without namespace logic.
26
+
27
+ Example implementations:
28
+ - SQLAlchemyStateStore: Uses State table
29
+ - RedisStateStore: Uses Redis
30
+ - MemoryStateStore: In-memory dict
31
+ """
32
+
33
+ async def get(self, key: str) -> Any | None:
34
+ """Get value by key."""
35
+ ...
36
+
37
+ async def set(self, key: str, value: Any) -> None:
38
+ """Set value by key."""
39
+ ...
40
+
41
+ async def delete(self, key: str) -> bool:
42
+ """Delete value by key. Returns True if deleted."""
43
+ ...
44
+
45
+ async def list(self, prefix: str = "") -> list[str]:
46
+ """List keys with optional prefix filter."""
47
+ ...
48
+
49
+ async def exists(self, key: str) -> bool:
50
+ """Check if key exists."""
51
+ ...
52
+
53
+
54
+ @runtime_checkable
55
+ class StateBackend(Protocol):
56
+ """Protocol for generic key-value state storage.
57
+
58
+ Provides simple key-value storage with namespace isolation.
59
+ Use for storing arbitrary state that doesn't fit other backends.
60
+
61
+ Example namespaces:
62
+ - "plan" - Plan state
63
+ - "config" - Configuration
64
+ - "cache" - Temporary cache
65
+ - "workflow" - Workflow state
66
+
67
+ Example usage:
68
+ # Store plan state
69
+ await backend.set("plan", "sess_123:plan", {"status": "active"})
70
+
71
+ # Get plan state
72
+ plan = await backend.get("plan", "sess_123:plan")
73
+
74
+ # List all plans
75
+ keys = await backend.list("plan")
76
+ """
77
+
78
+ async def get(self, namespace: str, key: str) -> Any | None:
79
+ """Get value by key.
80
+
81
+ Args:
82
+ namespace: Namespace for isolation
83
+ key: Storage key
84
+
85
+ Returns:
86
+ Stored value or None if not found
87
+ """
88
+ ...
89
+
90
+ async def set(self, namespace: str, key: str, value: Any) -> None:
91
+ """Set value by key.
92
+
93
+ Args:
94
+ namespace: Namespace for isolation
95
+ key: Storage key
96
+ value: Value to store (must be JSON-serializable)
97
+ """
98
+ ...
99
+
100
+ async def delete(self, namespace: str, key: str) -> bool:
101
+ """Delete value by key.
102
+
103
+ Args:
104
+ namespace: Namespace
105
+ key: Storage key
106
+
107
+ Returns:
108
+ True if deleted, False if not found
109
+ """
110
+ ...
111
+
112
+ async def list(self, namespace: str, prefix: str = "") -> list[str]:
113
+ """List keys with optional prefix filter.
114
+
115
+ Args:
116
+ namespace: Namespace
117
+ prefix: Optional key prefix filter
118
+
119
+ Returns:
120
+ List of matching keys
121
+ """
122
+ ...
123
+
124
+ async def exists(self, namespace: str, key: str) -> bool:
125
+ """Check if key exists.
126
+
127
+ Args:
128
+ namespace: Namespace
129
+ key: Storage key
130
+
131
+ Returns:
132
+ True if exists
133
+ """
134
+ ...
135
+
136
+
137
+ class StoreBasedStateBackend:
138
+ """StateBackend implementation that wraps a StateStore.
139
+
140
+ This allows API layer to implement only the simple StateStore interface,
141
+ while the framework provides namespace handling.
142
+
143
+ Example:
144
+ # API layer implements StateStore
145
+ store = SQLAlchemyStateStore(session_factory, tenant_id)
146
+
147
+ # Framework wraps it as StateBackend
148
+ backend = StoreBasedStateBackend(store)
149
+ """
150
+
151
+ def __init__(self, store: StateStore):
152
+ self._store = store
153
+
154
+ def _make_key(self, namespace: str, key: str) -> str:
155
+ """Combine namespace and key."""
156
+ return f"{namespace}:{key}"
157
+
158
+ async def get(self, namespace: str, key: str) -> Any | None:
159
+ return await self._store.get(self._make_key(namespace, key))
160
+
161
+ async def set(self, namespace: str, key: str, value: Any) -> None:
162
+ await self._store.set(self._make_key(namespace, key), value)
163
+
164
+ async def delete(self, namespace: str, key: str) -> bool:
165
+ return await self._store.delete(self._make_key(namespace, key))
166
+
167
+ async def list(self, namespace: str, prefix: str = "") -> list[str]:
168
+ full_prefix = self._make_key(namespace, prefix)
169
+ keys = await self._store.list(full_prefix)
170
+ # Strip namespace prefix from results
171
+ ns_prefix = f"{namespace}:"
172
+ return [k[len(ns_prefix):] for k in keys if k.startswith(ns_prefix)]
173
+
174
+ async def exists(self, namespace: str, key: str) -> bool:
175
+ return await self._store.exists(self._make_key(namespace, key))
176
+
177
+
178
+ __all__ = ["StateBackend", "StateStore", "StoreBasedStateBackend"]
@@ -0,0 +1,165 @@
1
+ """SubAgent backend for sub-agent registry and retrieval.
2
+
3
+ Supports different agent retrieval strategies:
4
+ - ListSubAgentBackend: Static list of agents
5
+ - Custom backends: DB, API, dynamic discovery
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Literal, Protocol, TYPE_CHECKING, runtime_checkable
11
+
12
+ if TYPE_CHECKING:
13
+ from ...core.base import BaseAgent
14
+
15
+
16
+ @dataclass
17
+ class AgentConfig:
18
+ """Configuration for a sub-agent.
19
+
20
+ 行为配置:
21
+ - inherit_messages: 是否继承父 agent 的消息历史
22
+ - return_to_parent: 完成后是否返回结果给父 agent
23
+ - create_invocation: 是否创建新 invocation(用户可直接交互)
24
+
25
+ 状态配置:
26
+ - inherit_state: 继承哪些状态 ("all", "none", 或具体 key 列表)
27
+ - return_state_keys: 返回哪些状态给父 agent
28
+ - summary_mode: 返回摘要模式 ("full", "truncate", "none")
29
+
30
+ 手动切换配置:
31
+ - switchable: 是否出现在前端可切换列表
32
+ - display_name: 前端显示名称
33
+
34
+ 超时配置:
35
+ - timeout: 执行超时时间(秒),收到事件时会刷新
36
+ """
37
+ key: str
38
+ agent: Any # BaseAgent instance or class
39
+ description: str = ""
40
+
41
+ # 行为配置(替代原 mode)
42
+ inherit_messages: bool = False
43
+ return_to_parent: bool = True
44
+ create_invocation: bool = False
45
+
46
+ # 状态配置
47
+ inherit_state: Literal["all", "none"] | list[str] = "none"
48
+ return_state_keys: list[str] = field(default_factory=list)
49
+ summary_mode: Literal["full", "truncate", "none"] = "full"
50
+
51
+ # 手动切换配置
52
+ switchable: bool = False
53
+ display_name: str = ""
54
+
55
+ # 超时配置(默认 5 分钟,收到事件时刷新)
56
+ timeout: float = 300.0
57
+
58
+ # 消息记录配置(可选覆盖,默认由 return_to_parent 派生)
59
+ # - return_to_parent=True → 不记录(消息归并到父级结果)
60
+ # - return_to_parent=False → 记录到独立 namespace
61
+ _record_messages: bool | None = None # None = 用默认派生逻辑
62
+
63
+ # Permission config (optional override)
64
+ permission_config: Any | None = None
65
+
66
+ @property
67
+ def record_messages(self) -> bool:
68
+ """是否记录消息 - 默认由 return_to_parent 派生.
69
+
70
+ - return_to_parent=True → 不记录(结果归并到父级)
71
+ - return_to_parent=False → 记录(独立运行)
72
+ """
73
+ if self._record_messages is not None:
74
+ return self._record_messages
75
+ return not self.return_to_parent
76
+
77
+ @property
78
+ def mode(self) -> Literal["embedded", "delegated"]:
79
+ """兼容属性:根据 create_invocation 返回 mode."""
80
+ return "delegated" if self.create_invocation else "embedded"
81
+
82
+ def to_dict(self) -> dict:
83
+ return {
84
+ "key": self.key,
85
+ "agent_name": getattr(self.agent, "name", str(self.agent)),
86
+ "mode": self.mode,
87
+ "description": self.description or getattr(self.agent, "description", ""),
88
+ "inherit_messages": self.inherit_messages,
89
+ "return_to_parent": self.return_to_parent,
90
+ "create_invocation": self.create_invocation,
91
+ "switchable": self.switchable,
92
+ "display_name": self.display_name,
93
+ }
94
+
95
+
96
+ @runtime_checkable
97
+ class SubAgentBackend(Protocol):
98
+ """Protocol for sub-agent retrieval."""
99
+
100
+ async def get(self, key: str) -> AgentConfig | None:
101
+ """Get agent config by key."""
102
+ ...
103
+
104
+ async def list(self) -> list[AgentConfig]:
105
+ """List all available agents."""
106
+ ...
107
+
108
+
109
+ class ListSubAgentBackend:
110
+ """Default implementation: Static list of agents."""
111
+
112
+ def __init__(self, agents: list[AgentConfig] | None = None):
113
+ self._agents: dict[str, AgentConfig] = {}
114
+ if agents:
115
+ for config in agents:
116
+ self._agents[config.key] = config
117
+
118
+ def register(self, config: AgentConfig) -> None:
119
+ """Register an agent config."""
120
+ self._agents[config.key] = config
121
+
122
+ def register_agent(
123
+ self,
124
+ key: str,
125
+ agent: Any,
126
+ description: str = "",
127
+ *,
128
+ inherit_messages: bool = False,
129
+ return_to_parent: bool = True,
130
+ create_invocation: bool = False,
131
+ switchable: bool = False,
132
+ display_name: str = "",
133
+ ) -> None:
134
+ """Convenience method to register an agent."""
135
+ self._agents[key] = AgentConfig(
136
+ key=key,
137
+ agent=agent,
138
+ description=description or getattr(agent, "description", ""),
139
+ inherit_messages=inherit_messages,
140
+ return_to_parent=return_to_parent,
141
+ create_invocation=create_invocation,
142
+ switchable=switchable,
143
+ display_name=display_name,
144
+ )
145
+
146
+ async def get(self, key: str) -> AgentConfig | None:
147
+ return self._agents.get(key)
148
+
149
+ async def list(self) -> list[AgentConfig]:
150
+ return list(self._agents.values())
151
+
152
+ def list_sync(self) -> list[AgentConfig]:
153
+ """Synchronous version of list() for use in property getters."""
154
+ return list(self._agents.values())
155
+
156
+ def clear(self) -> None:
157
+ """Clear all agents."""
158
+ self._agents.clear()
159
+
160
+
161
+ __all__ = [
162
+ "SubAgentBackend",
163
+ "AgentConfig",
164
+ "ListSubAgentBackend",
165
+ ]
@@ -0,0 +1,41 @@
1
+ """
2
+ Aury Agent CLI - 命令行界面
3
+ ===========================
4
+
5
+ 提供命令行工具用于:
6
+ - 交互式对话 (chat)
7
+ - 运行 Workflow (workflow)
8
+ - 管理会话 (session)
9
+ - 配置管理 (config)
10
+
11
+ 扩展支持:
12
+ - 注册自定义 Agent
13
+ - 注册自定义 Tool
14
+ - 从配置文件加载扩展
15
+ - 从 entry points 加载扩展
16
+ """
17
+ from aury.agents.cli.main import app, main
18
+ from aury.agents.cli.extensions import (
19
+ ExtensionRegistry,
20
+ ExtensionInfo,
21
+ registry,
22
+ register_agent,
23
+ register_tool,
24
+ get_agent,
25
+ get_tool,
26
+ auto_discover,
27
+ )
28
+
29
+ __all__ = [
30
+ "app",
31
+ "main",
32
+ # Extensions
33
+ "ExtensionRegistry",
34
+ "ExtensionInfo",
35
+ "registry",
36
+ "register_agent",
37
+ "register_tool",
38
+ "get_agent",
39
+ "get_tool",
40
+ "auto_discover",
41
+ ]