crewlayer 0.1.0__tar.gz

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.
@@ -0,0 +1,232 @@
1
+ Metadata-Version: 2.4
2
+ Name: crewlayer
3
+ Version: 0.1.0
4
+ Summary: Open source memory & context backend for AI agents
5
+ Project-URL: Homepage, https://github.com/GerardSole/CrewLayer
6
+ Project-URL: Repository, https://github.com/GerardSole/CrewLayer
7
+ Project-URL: Documentation, https://github.com/GerardSole/CrewLayer/blob/main/sdk/README.md
8
+ Project-URL: Bug Tracker, https://github.com/GerardSole/CrewLayer/issues
9
+ Author-email: Gerard Solé <gesoca2003@gmail.com>
10
+ License: MIT
11
+ Keywords: agents,ai,autogen,context,crewai,crewlayer,langchain,llamaindex,llm,memory
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: httpx<1.0,>=0.27.0
24
+ Provides-Extra: all-integrations
25
+ Requires-Dist: crewai>=0.28.0; extra == 'all-integrations'
26
+ Requires-Dist: langchain-core<1.0,>=0.2.0; extra == 'all-integrations'
27
+ Requires-Dist: llama-index-core>=0.10.0; extra == 'all-integrations'
28
+ Requires-Dist: pyautogen>=0.2.0; extra == 'all-integrations'
29
+ Provides-Extra: autogen
30
+ Requires-Dist: pyautogen>=0.2.0; extra == 'autogen'
31
+ Provides-Extra: build
32
+ Requires-Dist: build>=1.2; extra == 'build'
33
+ Requires-Dist: twine>=6.0; extra == 'build'
34
+ Provides-Extra: crewai
35
+ Requires-Dist: crewai>=0.28.0; extra == 'crewai'
36
+ Provides-Extra: dev
37
+ Requires-Dist: httpx<1.0,>=0.27.0; extra == 'dev'
38
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
39
+ Requires-Dist: pytest>=8.0; extra == 'dev'
40
+ Provides-Extra: langchain
41
+ Requires-Dist: langchain-core<1.0,>=0.2.0; extra == 'langchain'
42
+ Provides-Extra: llamaindex
43
+ Requires-Dist: llama-index-core>=0.10.0; extra == 'llamaindex'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # CrewLayer Python SDK
47
+
48
+ Open source memory & context backend for AI agents.
49
+ Persistent memory, action logging, and shared blackboard — all in one REST API.
50
+
51
+ [![PyPI](https://img.shields.io/pypi/v/crewlayer)](https://pypi.org/project/crewlayer/)
52
+ [![Python](https://img.shields.io/pypi/pyversions/crewlayer)](https://pypi.org/project/crewlayer/)
53
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/GerardSole/CrewLayer/blob/main/LICENSE)
54
+
55
+ **Source & docs:** [github.com/GerardSole/CrewLayer](https://github.com/GerardSole/CrewLayer)
56
+
57
+ ---
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install crewlayer
63
+ ```
64
+
65
+ Requires Python 3.12+. Runtime dependency: `httpx` only.
66
+
67
+ ---
68
+
69
+ ## Quick start
70
+
71
+ ```python
72
+ from crewlayer import CrewLayerClient
73
+
74
+ client = CrewLayerClient(api_key="crwl_...", base_url="http://localhost:8000")
75
+
76
+ # Persist a message to short-term memory
77
+ client.memory.append(agent_id="agent-uuid", role="user", content="I prefer dark mode")
78
+
79
+ # Semantic recall from long-term memory
80
+ results = client.memory.recall(agent_id="agent-uuid", query="UI preferences", limit=5)
81
+ for item in results.results:
82
+ print(f"[{item.similarity:.2f}] {item.content}")
83
+
84
+ # Log an action (full audit trail)
85
+ client.actions.log(agent_id="agent-uuid", tool_name="web_search",
86
+ input_params={"q": "crewlayer"}, status="success", duration_ms=120)
87
+
88
+ # Shared blackboard between agents
89
+ client.context.write(namespace="project-42", key="phase", value={"stage": "planning"})
90
+ entry = client.context.read("project-42", "phase")
91
+ print(entry.value) # {"stage": "planning"}
92
+
93
+ client.close()
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Async client
99
+
100
+ ```python
101
+ import asyncio
102
+ from crewlayer import CrewLayerAsyncClient
103
+
104
+ async def main():
105
+ async with CrewLayerAsyncClient(api_key="crwl_...") as client:
106
+ await client.memory.append(agent_id="agent-uuid", role="user", content="Hello")
107
+ result = await client.memory.recall(agent_id="agent-uuid", query="greeting")
108
+ print(result.results)
109
+
110
+ asyncio.run(main())
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Integrations
116
+
117
+ Optional extras bring first-class support for popular AI frameworks.
118
+ Each integration falls back gracefully when the framework is not installed.
119
+
120
+ | Extra | Install | What you get |
121
+ |---|---|---|
122
+ | `langchain` | `pip install crewlayer[langchain]` | `AgentLayerMemory`, `AgentLayerVectorStore`, `AgentLayerCallbackHandler` |
123
+ | `crewai` | `pip install crewlayer[crewai]` | `AgentLayerMemoryProvider`, `AgentLayerTaskLogger` |
124
+ | `llamaindex` | `pip install crewlayer[llamaindex]` | `CrewLayerMemoryBuffer`, `CrewLayerVectorIndex`, `CrewLayerQueryEngine`, `CrewLayerCallbackManager` |
125
+ | `autogen` | `pip install crewlayer[autogen]` | `CrewLayerConversableAgent`, `CrewLayerGroupChatManager`, `CrewLayerAgentMemory`, `sync_agent_status` |
126
+ | `all-integrations` | `pip install crewlayer[all-integrations]` | All of the above |
127
+
128
+ ### LangChain
129
+
130
+ ```python
131
+ from crewlayer import CrewLayerClient
132
+ from crewlayer.integrations.langchain import AgentLayerMemory
133
+ from langchain.chains import ConversationChain
134
+ from langchain_openai import ChatOpenAI
135
+
136
+ client = CrewLayerClient(api_key="crwl_...")
137
+ memory = AgentLayerMemory(client=client, agent_id="agent-uuid", session_id="user-123")
138
+ chain = ConversationChain(llm=ChatOpenAI(), memory=memory)
139
+ chain.predict(input="What's my name?")
140
+ ```
141
+
142
+ ### CrewAI
143
+
144
+ ```python
145
+ from crewlayer.integrations.crewai import AgentLayerMemoryProvider, AgentLayerTaskLogger
146
+ from crewai.memory import LongTermMemory
147
+ from crewai import Task
148
+
149
+ storage = AgentLayerMemoryProvider(client=client, agent_id="agent-uuid")
150
+ ltm = LongTermMemory(storage=storage)
151
+
152
+ logger = AgentLayerTaskLogger(client=client, agent_id="agent-uuid")
153
+ task = Task(description="Summarize feedback", expected_output="...", agent=agent, callback=logger)
154
+ ```
155
+
156
+ ### LlamaIndex
157
+
158
+ ```python
159
+ from crewlayer.integrations.llamaindex import CrewLayerVectorIndex
160
+ from llama_index.core.schema import Document
161
+
162
+ index = CrewLayerVectorIndex(client=client, agent_id="agent-uuid", similarity_top_k=4)
163
+ index.insert(Document(text="User prefers dark mode"))
164
+ engine = index.as_query_engine()
165
+ response = engine.query("UI preferences")
166
+ print(response.response)
167
+ ```
168
+
169
+ ### AutoGen (multi-agent blackboard)
170
+
171
+ The killer feature: `CrewLayerGroupChatManager` writes every turn to a shared blackboard.
172
+ Any agent — or external observer — can read live group state without being in the chat.
173
+
174
+ ```python
175
+ from crewlayer.integrations.autogen import (
176
+ CrewLayerConversableAgent, CrewLayerGroupChatManager, CrewLayerAgentMemory,
177
+ )
178
+ import autogen
179
+
180
+ client = CrewLayerClient(api_key="crwl_...")
181
+ researcher = CrewLayerConversableAgent(name="researcher", client=client, agent_id="uuid-r",
182
+ llm_config={"config_list": [...]})
183
+ writer = CrewLayerConversableAgent(name="writer", client=client, agent_id="uuid-w",
184
+ llm_config={"config_list": [...]})
185
+
186
+ groupchat = autogen.GroupChat(agents=[researcher, writer], messages=[], max_round=10)
187
+ manager = CrewLayerGroupChatManager(client=client, group_id="project-alpha", groupchat=groupchat)
188
+ CrewLayerAgentMemory(client=client, agent_id="uuid-r").apply(researcher)
189
+
190
+ researcher.initiate_chat(manager, message="Let's plan the release.")
191
+
192
+ # From anywhere — see who spoke last
193
+ latest = client.context.read("project-alpha", "latest_turn")
194
+ print(latest.value) # {"agent": "writer", "content": "...", "turn": 3}
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Error handling
200
+
201
+ ```python
202
+ from crewlayer import CrewLayerError, AuthError, NotFoundError, ConflictError, RateLimitError
203
+
204
+ try:
205
+ client.memory.recall(agent_id="bad-id", query="test")
206
+ except AuthError:
207
+ print("Invalid API key")
208
+ except NotFoundError:
209
+ print("Agent not found")
210
+ except ConflictError as e:
211
+ print(f"Version conflict: {e}")
212
+ except RateLimitError:
213
+ print("Rate limited")
214
+ except CrewLayerError as e:
215
+ print(f"HTTP {e.status_code}: {e}")
216
+ ```
217
+
218
+ All exceptions expose `.status_code` (int | None) and `.response` (dict | None).
219
+
220
+ ---
221
+
222
+ ## Self-hosting
223
+
224
+ ```bash
225
+ git clone https://github.com/GerardSole/CrewLayer
226
+ cd CrewLayer
227
+ docker compose up -d # starts PostgreSQL + Redis
228
+ alembic upgrade head
229
+ uvicorn main:app --reload # API at http://localhost:8000
230
+ ```
231
+
232
+ Full documentation: [github.com/GerardSole/CrewLayer](https://github.com/GerardSole/CrewLayer)
@@ -0,0 +1,187 @@
1
+ # CrewLayer Python SDK
2
+
3
+ Open source memory & context backend for AI agents.
4
+ Persistent memory, action logging, and shared blackboard — all in one REST API.
5
+
6
+ [![PyPI](https://img.shields.io/pypi/v/crewlayer)](https://pypi.org/project/crewlayer/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/crewlayer)](https://pypi.org/project/crewlayer/)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/GerardSole/CrewLayer/blob/main/LICENSE)
9
+
10
+ **Source & docs:** [github.com/GerardSole/CrewLayer](https://github.com/GerardSole/CrewLayer)
11
+
12
+ ---
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install crewlayer
18
+ ```
19
+
20
+ Requires Python 3.12+. Runtime dependency: `httpx` only.
21
+
22
+ ---
23
+
24
+ ## Quick start
25
+
26
+ ```python
27
+ from crewlayer import CrewLayerClient
28
+
29
+ client = CrewLayerClient(api_key="crwl_...", base_url="http://localhost:8000")
30
+
31
+ # Persist a message to short-term memory
32
+ client.memory.append(agent_id="agent-uuid", role="user", content="I prefer dark mode")
33
+
34
+ # Semantic recall from long-term memory
35
+ results = client.memory.recall(agent_id="agent-uuid", query="UI preferences", limit=5)
36
+ for item in results.results:
37
+ print(f"[{item.similarity:.2f}] {item.content}")
38
+
39
+ # Log an action (full audit trail)
40
+ client.actions.log(agent_id="agent-uuid", tool_name="web_search",
41
+ input_params={"q": "crewlayer"}, status="success", duration_ms=120)
42
+
43
+ # Shared blackboard between agents
44
+ client.context.write(namespace="project-42", key="phase", value={"stage": "planning"})
45
+ entry = client.context.read("project-42", "phase")
46
+ print(entry.value) # {"stage": "planning"}
47
+
48
+ client.close()
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Async client
54
+
55
+ ```python
56
+ import asyncio
57
+ from crewlayer import CrewLayerAsyncClient
58
+
59
+ async def main():
60
+ async with CrewLayerAsyncClient(api_key="crwl_...") as client:
61
+ await client.memory.append(agent_id="agent-uuid", role="user", content="Hello")
62
+ result = await client.memory.recall(agent_id="agent-uuid", query="greeting")
63
+ print(result.results)
64
+
65
+ asyncio.run(main())
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Integrations
71
+
72
+ Optional extras bring first-class support for popular AI frameworks.
73
+ Each integration falls back gracefully when the framework is not installed.
74
+
75
+ | Extra | Install | What you get |
76
+ |---|---|---|
77
+ | `langchain` | `pip install crewlayer[langchain]` | `AgentLayerMemory`, `AgentLayerVectorStore`, `AgentLayerCallbackHandler` |
78
+ | `crewai` | `pip install crewlayer[crewai]` | `AgentLayerMemoryProvider`, `AgentLayerTaskLogger` |
79
+ | `llamaindex` | `pip install crewlayer[llamaindex]` | `CrewLayerMemoryBuffer`, `CrewLayerVectorIndex`, `CrewLayerQueryEngine`, `CrewLayerCallbackManager` |
80
+ | `autogen` | `pip install crewlayer[autogen]` | `CrewLayerConversableAgent`, `CrewLayerGroupChatManager`, `CrewLayerAgentMemory`, `sync_agent_status` |
81
+ | `all-integrations` | `pip install crewlayer[all-integrations]` | All of the above |
82
+
83
+ ### LangChain
84
+
85
+ ```python
86
+ from crewlayer import CrewLayerClient
87
+ from crewlayer.integrations.langchain import AgentLayerMemory
88
+ from langchain.chains import ConversationChain
89
+ from langchain_openai import ChatOpenAI
90
+
91
+ client = CrewLayerClient(api_key="crwl_...")
92
+ memory = AgentLayerMemory(client=client, agent_id="agent-uuid", session_id="user-123")
93
+ chain = ConversationChain(llm=ChatOpenAI(), memory=memory)
94
+ chain.predict(input="What's my name?")
95
+ ```
96
+
97
+ ### CrewAI
98
+
99
+ ```python
100
+ from crewlayer.integrations.crewai import AgentLayerMemoryProvider, AgentLayerTaskLogger
101
+ from crewai.memory import LongTermMemory
102
+ from crewai import Task
103
+
104
+ storage = AgentLayerMemoryProvider(client=client, agent_id="agent-uuid")
105
+ ltm = LongTermMemory(storage=storage)
106
+
107
+ logger = AgentLayerTaskLogger(client=client, agent_id="agent-uuid")
108
+ task = Task(description="Summarize feedback", expected_output="...", agent=agent, callback=logger)
109
+ ```
110
+
111
+ ### LlamaIndex
112
+
113
+ ```python
114
+ from crewlayer.integrations.llamaindex import CrewLayerVectorIndex
115
+ from llama_index.core.schema import Document
116
+
117
+ index = CrewLayerVectorIndex(client=client, agent_id="agent-uuid", similarity_top_k=4)
118
+ index.insert(Document(text="User prefers dark mode"))
119
+ engine = index.as_query_engine()
120
+ response = engine.query("UI preferences")
121
+ print(response.response)
122
+ ```
123
+
124
+ ### AutoGen (multi-agent blackboard)
125
+
126
+ The killer feature: `CrewLayerGroupChatManager` writes every turn to a shared blackboard.
127
+ Any agent — or external observer — can read live group state without being in the chat.
128
+
129
+ ```python
130
+ from crewlayer.integrations.autogen import (
131
+ CrewLayerConversableAgent, CrewLayerGroupChatManager, CrewLayerAgentMemory,
132
+ )
133
+ import autogen
134
+
135
+ client = CrewLayerClient(api_key="crwl_...")
136
+ researcher = CrewLayerConversableAgent(name="researcher", client=client, agent_id="uuid-r",
137
+ llm_config={"config_list": [...]})
138
+ writer = CrewLayerConversableAgent(name="writer", client=client, agent_id="uuid-w",
139
+ llm_config={"config_list": [...]})
140
+
141
+ groupchat = autogen.GroupChat(agents=[researcher, writer], messages=[], max_round=10)
142
+ manager = CrewLayerGroupChatManager(client=client, group_id="project-alpha", groupchat=groupchat)
143
+ CrewLayerAgentMemory(client=client, agent_id="uuid-r").apply(researcher)
144
+
145
+ researcher.initiate_chat(manager, message="Let's plan the release.")
146
+
147
+ # From anywhere — see who spoke last
148
+ latest = client.context.read("project-alpha", "latest_turn")
149
+ print(latest.value) # {"agent": "writer", "content": "...", "turn": 3}
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Error handling
155
+
156
+ ```python
157
+ from crewlayer import CrewLayerError, AuthError, NotFoundError, ConflictError, RateLimitError
158
+
159
+ try:
160
+ client.memory.recall(agent_id="bad-id", query="test")
161
+ except AuthError:
162
+ print("Invalid API key")
163
+ except NotFoundError:
164
+ print("Agent not found")
165
+ except ConflictError as e:
166
+ print(f"Version conflict: {e}")
167
+ except RateLimitError:
168
+ print("Rate limited")
169
+ except CrewLayerError as e:
170
+ print(f"HTTP {e.status_code}: {e}")
171
+ ```
172
+
173
+ All exceptions expose `.status_code` (int | None) and `.response` (dict | None).
174
+
175
+ ---
176
+
177
+ ## Self-hosting
178
+
179
+ ```bash
180
+ git clone https://github.com/GerardSole/CrewLayer
181
+ cd CrewLayer
182
+ docker compose up -d # starts PostgreSQL + Redis
183
+ alembic upgrade head
184
+ uvicorn main:app --reload # API at http://localhost:8000
185
+ ```
186
+
187
+ Full documentation: [github.com/GerardSole/CrewLayer](https://github.com/GerardSole/CrewLayer)
@@ -0,0 +1,54 @@
1
+ """CrewLayer SDK — official Python client for the CrewLayer AI agent backend."""
2
+ from crewlayer._client import CrewLayerAsyncClient, CrewLayerClient
3
+ from crewlayer._exceptions import (
4
+ AuthError,
5
+ ConflictError,
6
+ CrewLayerError,
7
+ NotFoundError,
8
+ RateLimitError,
9
+ ServerError,
10
+ )
11
+ from crewlayer._types import (
12
+ ActionPage,
13
+ ActionRecord,
14
+ ActionStats,
15
+ ContextEntry,
16
+ ContextNamespace,
17
+ ExtractResult,
18
+ MemoryItem,
19
+ MemoryPage,
20
+ Message,
21
+ RecallResult,
22
+ ShortMemory,
23
+ ToolStat,
24
+ )
25
+
26
+ __all__ = [
27
+ # Clients
28
+ "CrewLayerClient",
29
+ "CrewLayerAsyncClient",
30
+ # Exceptions
31
+ "CrewLayerError",
32
+ "AuthError",
33
+ "NotFoundError",
34
+ "ConflictError",
35
+ "RateLimitError",
36
+ "ServerError",
37
+ # Types — memory
38
+ "Message",
39
+ "ShortMemory",
40
+ "MemoryItem",
41
+ "RecallResult",
42
+ "ExtractResult",
43
+ "MemoryPage",
44
+ # Types — actions
45
+ "ActionRecord",
46
+ "ActionPage",
47
+ "ToolStat",
48
+ "ActionStats",
49
+ # Types — context
50
+ "ContextEntry",
51
+ "ContextNamespace",
52
+ ]
53
+
54
+ __version__ = "0.1.0"
@@ -0,0 +1,153 @@
1
+ """Actions resource clients — sync and async."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from crewlayer._http import AsyncTransport, SyncTransport
7
+ from crewlayer._types import ActionPage, ActionRecord, ActionStats
8
+
9
+
10
+ class ActionsClient:
11
+ """Synchronous action log operations."""
12
+
13
+ def __init__(self, http: SyncTransport) -> None:
14
+ self._http = http
15
+
16
+ def log(
17
+ self,
18
+ agent_id: str,
19
+ tool_name: str,
20
+ input_params: dict[str, Any],
21
+ output_result: dict[str, Any],
22
+ status: str,
23
+ *,
24
+ session_id: str | None = None,
25
+ duration_ms: int | None = None,
26
+ error_msg: str | None = None,
27
+ metadata: dict[str, Any] | None = None,
28
+ ) -> ActionRecord:
29
+ """Record an immutable action entry for the agent."""
30
+ data = self._http.request(
31
+ "POST",
32
+ f"/v1/agents/{agent_id}/actions",
33
+ json={
34
+ "tool_name": tool_name,
35
+ "input_params": input_params,
36
+ "output_result": output_result,
37
+ "status": status,
38
+ "session_id": session_id,
39
+ "duration_ms": duration_ms,
40
+ "error_msg": error_msg,
41
+ "metadata": metadata or {},
42
+ },
43
+ )
44
+ return ActionRecord._from(data)
45
+
46
+ def get(self, agent_id: str, action_id: str) -> ActionRecord:
47
+ """Retrieve a single action by ID."""
48
+ data = self._http.request("GET", f"/v1/agents/{agent_id}/actions/{action_id}")
49
+ return ActionRecord._from(data)
50
+
51
+ def list(
52
+ self,
53
+ agent_id: str,
54
+ *,
55
+ tool: str | None = None,
56
+ status: str | None = None,
57
+ since: str | None = None,
58
+ until: str | None = None,
59
+ limit: int = 50,
60
+ cursor: str | None = None,
61
+ ) -> ActionPage:
62
+ """List actions with optional filters. Paginated via cursor."""
63
+ params: dict[str, Any] = {"limit": limit}
64
+ if tool is not None:
65
+ params["tool"] = tool
66
+ if status is not None:
67
+ params["status"] = status
68
+ if since is not None:
69
+ params["since"] = since
70
+ if until is not None:
71
+ params["until"] = until
72
+ if cursor is not None:
73
+ params["cursor"] = cursor
74
+ data = self._http.request("GET", f"/v1/agents/{agent_id}/actions", params=params)
75
+ return ActionPage._from(data)
76
+
77
+ def stats(self, agent_id: str) -> ActionStats:
78
+ """Aggregate statistics: totals, error rate, average duration, per-tool breakdown."""
79
+ data = self._http.request("GET", f"/v1/agents/{agent_id}/actions/stats")
80
+ return ActionStats._from(data)
81
+
82
+
83
+ class AsyncActionsClient:
84
+ """Asynchronous action log operations."""
85
+
86
+ def __init__(self, http: AsyncTransport) -> None:
87
+ self._http = http
88
+
89
+ async def log(
90
+ self,
91
+ agent_id: str,
92
+ tool_name: str,
93
+ input_params: dict[str, Any],
94
+ output_result: dict[str, Any],
95
+ status: str,
96
+ *,
97
+ session_id: str | None = None,
98
+ duration_ms: int | None = None,
99
+ error_msg: str | None = None,
100
+ metadata: dict[str, Any] | None = None,
101
+ ) -> ActionRecord:
102
+ """Record an immutable action entry for the agent."""
103
+ data = await self._http.request(
104
+ "POST",
105
+ f"/v1/agents/{agent_id}/actions",
106
+ json={
107
+ "tool_name": tool_name,
108
+ "input_params": input_params,
109
+ "output_result": output_result,
110
+ "status": status,
111
+ "session_id": session_id,
112
+ "duration_ms": duration_ms,
113
+ "error_msg": error_msg,
114
+ "metadata": metadata or {},
115
+ },
116
+ )
117
+ return ActionRecord._from(data)
118
+
119
+ async def get(self, agent_id: str, action_id: str) -> ActionRecord:
120
+ """Retrieve a single action by ID."""
121
+ data = await self._http.request("GET", f"/v1/agents/{agent_id}/actions/{action_id}")
122
+ return ActionRecord._from(data)
123
+
124
+ async def list(
125
+ self,
126
+ agent_id: str,
127
+ *,
128
+ tool: str | None = None,
129
+ status: str | None = None,
130
+ since: str | None = None,
131
+ until: str | None = None,
132
+ limit: int = 50,
133
+ cursor: str | None = None,
134
+ ) -> ActionPage:
135
+ """List actions with optional filters. Paginated via cursor."""
136
+ params: dict[str, Any] = {"limit": limit}
137
+ if tool is not None:
138
+ params["tool"] = tool
139
+ if status is not None:
140
+ params["status"] = status
141
+ if since is not None:
142
+ params["since"] = since
143
+ if until is not None:
144
+ params["until"] = until
145
+ if cursor is not None:
146
+ params["cursor"] = cursor
147
+ data = await self._http.request("GET", f"/v1/agents/{agent_id}/actions", params=params)
148
+ return ActionPage._from(data)
149
+
150
+ async def stats(self, agent_id: str) -> ActionStats:
151
+ """Aggregate statistics: totals, error rate, average duration, per-tool breakdown."""
152
+ data = await self._http.request("GET", f"/v1/agents/{agent_id}/actions/stats")
153
+ return ActionStats._from(data)