acontext 0.0.17__py3-none-any.whl → 0.1.0__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.
acontext/agent/skill.py CHANGED
@@ -2,15 +2,126 @@
2
2
  Skill tools for agent operations.
3
3
  """
4
4
 
5
- from dataclasses import dataclass
5
+ from dataclasses import dataclass, field
6
6
 
7
7
  from .base import BaseContext, BaseTool, BaseToolPool
8
8
  from ..client import AcontextClient
9
+ from ..async_client import AcontextAsyncClient
10
+ from ..types.skill import Skill
9
11
 
10
12
 
11
13
  @dataclass
12
14
  class SkillContext(BaseContext):
15
+ """Context for skill tools with preloaded skill name mapping."""
16
+
13
17
  client: AcontextClient
18
+ skills: dict[str, Skill] = field(default_factory=dict)
19
+
20
+ @classmethod
21
+ def create(cls, client: AcontextClient, skill_ids: list[str]) -> "SkillContext":
22
+ """Create a SkillContext by preloading skills from a list of skill IDs.
23
+
24
+ Args:
25
+ client: The Acontext client instance.
26
+ skill_ids: List of skill UUIDs to preload.
27
+
28
+ Returns:
29
+ SkillContext with preloaded skills mapped by name.
30
+
31
+ Raises:
32
+ ValueError: If duplicate skill names are found.
33
+ """
34
+ skills: dict[str, Skill] = {}
35
+ for skill_id in skill_ids:
36
+ skill = client.skills.get(skill_id)
37
+ if skill.name in skills:
38
+ raise ValueError(
39
+ f"Duplicate skill name '{skill.name}' found. "
40
+ f"Existing ID: {skills[skill.name].id}, New ID: {skill.id}"
41
+ )
42
+ skills[skill.name] = skill
43
+ return cls(client=client, skills=skills)
44
+
45
+ def get_skill(self, skill_name: str) -> Skill:
46
+ """Get a skill by name from the preloaded skills.
47
+
48
+ Args:
49
+ skill_name: The name of the skill.
50
+
51
+ Returns:
52
+ The Skill object.
53
+
54
+ Raises:
55
+ ValueError: If the skill is not found in the context.
56
+ """
57
+ if skill_name not in self.skills:
58
+ available = ", ".join(self.skills.keys()) if self.skills else "[none]"
59
+ raise ValueError(
60
+ f"Skill '{skill_name}' not found in context. Available skills: {available}"
61
+ )
62
+ return self.skills[skill_name]
63
+
64
+ def list_skill_names(self) -> list[str]:
65
+ """Return list of available skill names in this context."""
66
+ return list(self.skills.keys())
67
+
68
+
69
+ @dataclass
70
+ class AsyncSkillContext(BaseContext):
71
+ """Async context for skill tools with preloaded skill name mapping."""
72
+
73
+ client: AcontextAsyncClient
74
+ skills: dict[str, Skill] = field(default_factory=dict)
75
+
76
+ @classmethod
77
+ async def create(
78
+ cls, client: AcontextAsyncClient, skill_ids: list[str]
79
+ ) -> "AsyncSkillContext":
80
+ """Create an AsyncSkillContext by preloading skills from a list of skill IDs.
81
+
82
+ Args:
83
+ client: The Acontext async client instance.
84
+ skill_ids: List of skill UUIDs to preload.
85
+
86
+ Returns:
87
+ AsyncSkillContext with preloaded skills mapped by name.
88
+
89
+ Raises:
90
+ ValueError: If duplicate skill names are found.
91
+ """
92
+ skills: dict[str, Skill] = {}
93
+ for skill_id in skill_ids:
94
+ skill = await client.skills.get(skill_id)
95
+ if skill.name in skills:
96
+ raise ValueError(
97
+ f"Duplicate skill name '{skill.name}' found. "
98
+ f"Existing ID: {skills[skill.name].id}, New ID: {skill.id}"
99
+ )
100
+ skills[skill.name] = skill
101
+ return cls(client=client, skills=skills)
102
+
103
+ def get_skill(self, skill_name: str) -> Skill:
104
+ """Get a skill by name from the preloaded skills.
105
+
106
+ Args:
107
+ skill_name: The name of the skill.
108
+
109
+ Returns:
110
+ The Skill object.
111
+
112
+ Raises:
113
+ ValueError: If the skill is not found in the context.
114
+ """
115
+ if skill_name not in self.skills:
116
+ available = ", ".join(self.skills.keys()) if self.skills else "[none]"
117
+ raise ValueError(
118
+ f"Skill '{skill_name}' not found in context. Available skills: {available}"
119
+ )
120
+ return self.skills[skill_name]
121
+
122
+ def list_skill_names(self) -> list[str]:
123
+ """Return list of available skill names in this context."""
124
+ return list(self.skills.keys())
14
125
 
15
126
 
16
127
  class GetSkillTool(BaseTool):
@@ -23,37 +134,70 @@ class GetSkillTool(BaseTool):
23
134
  @property
24
135
  def description(self) -> str:
25
136
  return (
26
- "Get a skill by its name. Return the skill information including the relative paths of the files and their mime type categories"
137
+ "Get a skill by its name. Returns the skill information including "
138
+ "the relative paths of the files and their mime type categories."
27
139
  )
28
140
 
29
141
  @property
30
142
  def arguments(self) -> dict:
31
143
  return {
32
- "name": {
144
+ "skill_name": {
33
145
  "type": "string",
34
- "description": "The name of the skill (unique within project).",
146
+ "description": "The name of the skill.",
35
147
  },
36
148
  }
37
149
 
38
150
  @property
39
151
  def required_arguments(self) -> list[str]:
40
- return ["name"]
152
+ return ["skill_name"]
41
153
 
42
154
  def execute(self, ctx: SkillContext, llm_arguments: dict) -> str:
43
155
  """Get a skill by name."""
44
- name = llm_arguments.get("name")
156
+ skill_name = llm_arguments.get("skill_name")
157
+
158
+ if not skill_name:
159
+ raise ValueError("skill_name is required")
160
+
161
+ skill = ctx.get_skill(skill_name)
162
+
163
+ file_count = len(skill.file_index)
164
+
165
+ # Format all files with path and MIME type
166
+ if skill.file_index:
167
+ file_list = "\n".join(
168
+ [
169
+ f" - {file_info.path} ({file_info.mime})"
170
+ for file_info in skill.file_index
171
+ ]
172
+ )
173
+ else:
174
+ file_list = " [NO FILES]"
175
+
176
+ return (
177
+ f"Skill: {skill.name} (ID: {skill.id})\n"
178
+ f"Description: {skill.description}\n"
179
+ f"Files: {file_count} file(s)\n"
180
+ f"{file_list}"
181
+ )
182
+
183
+ async def async_execute(self, ctx: AsyncSkillContext, llm_arguments: dict) -> str:
184
+ """Get a skill by name (async)."""
185
+ skill_name = llm_arguments.get("skill_name")
45
186
 
46
- if not name:
47
- raise ValueError("name is required")
187
+ if not skill_name:
188
+ raise ValueError("skill_name is required")
48
189
 
49
- skill = ctx.client.skills.get_by_name(name)
190
+ skill = ctx.get_skill(skill_name)
50
191
 
51
192
  file_count = len(skill.file_index)
52
-
193
+
53
194
  # Format all files with path and MIME type
54
195
  if skill.file_index:
55
196
  file_list = "\n".join(
56
- [f" - {file_info.path} ({file_info.mime})" for file_info in skill.file_index]
197
+ [
198
+ f" - {file_info.path} ({file_info.mime})"
199
+ for file_info in skill.file_index
200
+ ]
57
201
  )
58
202
  else:
59
203
  file_list = " [NO FILES]"
@@ -62,9 +206,7 @@ class GetSkillTool(BaseTool):
62
206
  f"Skill: {skill.name} (ID: {skill.id})\n"
63
207
  f"Description: {skill.description}\n"
64
208
  f"Files: {file_count} file(s)\n"
65
- f"{file_list}\n"
66
- f"Created: {skill.created_at}\n"
67
- f"Updated: {skill.updated_at}"
209
+ f"{file_list}"
68
210
  )
69
211
 
70
212
 
@@ -78,7 +220,9 @@ class GetSkillFileTool(BaseTool):
78
220
  @property
79
221
  def description(self) -> str:
80
222
  return (
81
- "Get a file from a skill by name. The file_path should be a relative path within the skill (e.g., 'scripts/extract_text.json'). "
223
+ "Get a file from a skill by name. The file_path should be a relative "
224
+ "path within the skill (e.g., 'scripts/extract_text.json')."
225
+ "Tips: SKILL.md is the first file you should read to understand the full picture of this skill's content."
82
226
  )
83
227
 
84
228
  @property
@@ -108,18 +252,61 @@ class GetSkillFileTool(BaseTool):
108
252
  file_path = llm_arguments.get("file_path")
109
253
  expire = llm_arguments.get("expire")
110
254
 
255
+ if not skill_name:
256
+ raise ValueError("skill_name is required")
111
257
  if not file_path:
112
258
  raise ValueError("file_path is required")
259
+
260
+ skill = ctx.get_skill(skill_name)
261
+
262
+ result = ctx.client.skills.get_file(
263
+ skill_id=skill.id,
264
+ file_path=file_path,
265
+ expire=expire,
266
+ )
267
+
268
+ output_parts = [
269
+ f"File '{result.path}' (MIME: {result.mime}) from skill '{skill_name}':"
270
+ ]
271
+
272
+ if result.content:
273
+ output_parts.append(f"\nContent (type: {result.content.type}):")
274
+ output_parts.append(result.content.raw)
275
+
276
+ if result.url:
277
+ expire_seconds = expire if expire is not None else 900
278
+ output_parts.append(
279
+ f"\nDownload URL (expires in {expire_seconds} seconds):"
280
+ )
281
+ output_parts.append(result.url)
282
+
283
+ if not result.content and not result.url:
284
+ return f"File '{result.path}' retrieved but no content or URL returned."
285
+
286
+ return "\n".join(output_parts)
287
+
288
+ async def async_execute(self, ctx: AsyncSkillContext, llm_arguments: dict) -> str:
289
+ """Get a skill file (async)."""
290
+ skill_name = llm_arguments.get("skill_name")
291
+ file_path = llm_arguments.get("file_path")
292
+ expire = llm_arguments.get("expire")
293
+
113
294
  if not skill_name:
114
295
  raise ValueError("skill_name is required")
296
+ if not file_path:
297
+ raise ValueError("file_path is required")
115
298
 
116
- result = ctx.client.skills.get_file_by_name(
117
- skill_name=skill_name,
299
+ skill = ctx.get_skill(skill_name)
300
+
301
+ result = await ctx.client.skills.get_file(
302
+ skill_id=skill.id,
118
303
  file_path=file_path,
119
304
  expire=expire,
120
305
  )
121
306
 
122
- output_parts = [f"File '{result.path}' (MIME: {result.mime}) from skill '{skill_name}':"]
307
+ output_parts = [
308
+ f"File '{result.path}' (MIME: {result.mime}) from skill '{skill_name}':"
309
+ ]
123
310
 
124
311
  if result.content:
125
312
  output_parts.append(f"\nContent (type: {result.content.type}):")
@@ -127,7 +314,9 @@ class GetSkillFileTool(BaseTool):
127
314
 
128
315
  if result.url:
129
316
  expire_seconds = expire if expire is not None else 900
130
- output_parts.append(f"\nDownload URL (expires in {expire_seconds} seconds):")
317
+ output_parts.append(
318
+ f"\nDownload URL (expires in {expire_seconds} seconds):"
319
+ )
131
320
  output_parts.append(result.url)
132
321
 
133
322
  if not result.content and not result.url:
@@ -136,13 +325,81 @@ class GetSkillFileTool(BaseTool):
136
325
  return "\n".join(output_parts)
137
326
 
138
327
 
328
+ class ListSkillsTool(BaseTool):
329
+ """Tool for listing available skills in the context."""
330
+
331
+ @property
332
+ def name(self) -> str:
333
+ return "list_skills"
334
+
335
+ @property
336
+ def description(self) -> str:
337
+ return "List all available skills in the current context with their names and descriptions."
338
+
339
+ @property
340
+ def arguments(self) -> dict:
341
+ return {}
342
+
343
+ @property
344
+ def required_arguments(self) -> list[str]:
345
+ return []
346
+
347
+ def execute(self, ctx: SkillContext, llm_arguments: dict) -> str:
348
+ """List all available skills."""
349
+ if not ctx.skills:
350
+ return "No skills available in the current context."
351
+
352
+ skill_list = []
353
+ for skill_name, skill in ctx.skills.items():
354
+ skill_list.append(f"- {skill_name}: {skill.description}")
355
+
356
+ return f"Available skills ({len(ctx.skills)}):\n" + "\n".join(skill_list)
357
+
358
+ async def async_execute(self, ctx: AsyncSkillContext, llm_arguments: dict) -> str:
359
+ """List all available skills (async)."""
360
+ if not ctx.skills:
361
+ return "No skills available in the current context."
362
+
363
+ skill_list = []
364
+ for skill_name, skill in ctx.skills.items():
365
+ skill_list.append(f"- {skill_name}: {skill.description}")
366
+
367
+ return f"Available skills ({len(ctx.skills)}):\n" + "\n".join(skill_list)
368
+
369
+
139
370
  class SkillToolPool(BaseToolPool):
140
371
  """Tool pool for skill operations on Acontext skills."""
141
372
 
142
- def format_context(self, client: AcontextClient) -> SkillContext:
143
- return SkillContext(client=client)
373
+ def format_context(
374
+ self, client: AcontextClient, skill_ids: list[str]
375
+ ) -> SkillContext:
376
+ """Create a SkillContext by preloading skills from a list of skill IDs.
377
+
378
+ Args:
379
+ client: The Acontext client instance.
380
+ skill_ids: List of skill UUIDs to preload.
381
+
382
+ Returns:
383
+ SkillContext with preloaded skills mapped by name.
384
+ """
385
+ return SkillContext.create(client=client, skill_ids=skill_ids)
386
+
387
+ async def async_format_context(
388
+ self, client: AcontextAsyncClient, skill_ids: list[str]
389
+ ) -> AsyncSkillContext:
390
+ """Create an AsyncSkillContext by preloading skills from a list of skill IDs.
391
+
392
+ Args:
393
+ client: The Acontext async client instance.
394
+ skill_ids: List of skill UUIDs to preload.
395
+
396
+ Returns:
397
+ AsyncSkillContext with preloaded skills mapped by name.
398
+ """
399
+ return await AsyncSkillContext.create(client=client, skill_ids=skill_ids)
144
400
 
145
401
 
146
402
  SKILL_TOOLS = SkillToolPool()
403
+ SKILL_TOOLS.add_tool(ListSkillsTool())
147
404
  SKILL_TOOLS.add_tool(GetSkillTool())
148
405
  SKILL_TOOLS.add_tool(GetSkillFileTool())
@@ -33,26 +33,28 @@ class AsyncDisksAPI:
33
33
  time_desc: bool | None = None,
34
34
  ) -> ListDisksOutput:
35
35
  """List all disks in the project.
36
-
36
+
37
37
  Args:
38
38
  user: Filter by user identifier. Defaults to None.
39
39
  limit: Maximum number of disks to return. Defaults to None.
40
40
  cursor: Cursor for pagination. Defaults to None.
41
41
  time_desc: Order by created_at descending if True, ascending if False. Defaults to None.
42
-
42
+
43
43
  Returns:
44
44
  ListDisksOutput containing the list of disks and pagination information.
45
45
  """
46
- params = build_params(user=user, limit=limit, cursor=cursor, time_desc=time_desc)
46
+ params = build_params(
47
+ user=user, limit=limit, cursor=cursor, time_desc=time_desc
48
+ )
47
49
  data = await self._requester.request("GET", "/disk", params=params or None)
48
50
  return ListDisksOutput.model_validate(data)
49
51
 
50
52
  async def create(self, *, user: str | None = None) -> Disk:
51
53
  """Create a new disk.
52
-
54
+
53
55
  Args:
54
56
  user: Optional user identifier string. Defaults to None.
55
-
57
+
56
58
  Returns:
57
59
  The created Disk object.
58
60
  """
@@ -64,7 +66,7 @@ class AsyncDisksAPI:
64
66
 
65
67
  async def delete(self, disk_id: str) -> None:
66
68
  """Delete a disk by its ID.
67
-
69
+
68
70
  Args:
69
71
  disk_id: The UUID of the disk to delete.
70
72
  """
@@ -79,20 +81,22 @@ class AsyncDiskArtifactsAPI:
79
81
  self,
80
82
  disk_id: str,
81
83
  *,
82
- file: FileUpload
83
- | tuple[str, BinaryIO | bytes]
84
- | tuple[str, BinaryIO | bytes, str],
84
+ file: (
85
+ FileUpload
86
+ | tuple[str, BinaryIO | bytes]
87
+ | tuple[str, BinaryIO | bytes, str]
88
+ ),
85
89
  file_path: str | None = None,
86
90
  meta: Mapping[str, Any] | None = None,
87
91
  ) -> Artifact:
88
92
  """Upload a file to create or update an artifact.
89
-
93
+
90
94
  Args:
91
95
  disk_id: The UUID of the disk.
92
96
  file: The file to upload (FileUpload object or tuple format).
93
97
  file_path: Directory path (not including filename), defaults to "/".
94
98
  meta: Custom metadata as JSON-serializable dict, defaults to None.
95
-
99
+
96
100
  Returns:
97
101
  Artifact containing the created/updated artifact information.
98
102
  """
@@ -122,7 +126,7 @@ class AsyncDiskArtifactsAPI:
122
126
  expire: int | None = None,
123
127
  ) -> GetArtifactResp:
124
128
  """Get an artifact by disk ID, file path, and filename.
125
-
129
+
126
130
  Args:
127
131
  disk_id: The UUID of the disk.
128
132
  file_path: Directory path (not including filename).
@@ -130,7 +134,7 @@ class AsyncDiskArtifactsAPI:
130
134
  with_public_url: Whether to include a presigned public URL. Defaults to None.
131
135
  with_content: Whether to include file content. Defaults to None.
132
136
  expire: URL expiration time in seconds. Defaults to None.
133
-
137
+
134
138
  Returns:
135
139
  GetArtifactResp containing the artifact and optionally public URL and content.
136
140
  """
@@ -141,7 +145,9 @@ class AsyncDiskArtifactsAPI:
141
145
  with_content=with_content,
142
146
  expire=expire,
143
147
  )
144
- data = await self._requester.request("GET", f"/disk/{disk_id}/artifact", params=params)
148
+ data = await self._requester.request(
149
+ "GET", f"/disk/{disk_id}/artifact", params=params
150
+ )
145
151
  return GetArtifactResp.model_validate(data)
146
152
 
147
153
  async def update(
@@ -153,13 +159,13 @@ class AsyncDiskArtifactsAPI:
153
159
  meta: Mapping[str, Any],
154
160
  ) -> UpdateArtifactResp:
155
161
  """Update an artifact's metadata.
156
-
162
+
157
163
  Args:
158
164
  disk_id: The UUID of the disk.
159
165
  file_path: Directory path (not including filename).
160
166
  filename: The filename of the artifact.
161
167
  meta: Custom metadata as JSON-serializable dict.
162
-
168
+
163
169
  Returns:
164
170
  UpdateArtifactResp containing the updated artifact information.
165
171
  """
@@ -168,7 +174,9 @@ class AsyncDiskArtifactsAPI:
168
174
  "file_path": full_path,
169
175
  "meta": json.dumps(cast(Mapping[str, Any], meta)),
170
176
  }
171
- data = await self._requester.request("PUT", f"/disk/{disk_id}/artifact", json_data=payload)
177
+ data = await self._requester.request(
178
+ "PUT", f"/disk/{disk_id}/artifact", json_data=payload
179
+ )
172
180
  return UpdateArtifactResp.model_validate(data)
173
181
 
174
182
  async def delete(
@@ -179,7 +187,7 @@ class AsyncDiskArtifactsAPI:
179
187
  filename: str,
180
188
  ) -> None:
181
189
  """Delete an artifact by disk ID, file path, and filename.
182
-
190
+
183
191
  Args:
184
192
  disk_id: The UUID of the disk.
185
193
  file_path: Directory path (not including filename).
@@ -187,7 +195,9 @@ class AsyncDiskArtifactsAPI:
187
195
  """
188
196
  full_path = f"{file_path.rstrip('/')}/{filename}"
189
197
  params = {"file_path": full_path}
190
- await self._requester.request("DELETE", f"/disk/{disk_id}/artifact", params=params)
198
+ await self._requester.request(
199
+ "DELETE", f"/disk/{disk_id}/artifact", params=params
200
+ )
191
201
 
192
202
  async def list(
193
203
  self,
@@ -195,9 +205,83 @@ class AsyncDiskArtifactsAPI:
195
205
  *,
196
206
  path: str | None = None,
197
207
  ) -> ListArtifactsResp:
208
+ """List artifacts in a disk at a specific path.
209
+
210
+ Args:
211
+ disk_id: The UUID of the disk.
212
+ path: Directory path to list. Defaults to None (root).
213
+
214
+ Returns:
215
+ ListArtifactsResp containing the list of artifacts.
216
+ """
198
217
  params: dict[str, Any] = {}
199
218
  if path is not None:
200
219
  params["path"] = path
201
- data = await self._requester.request("GET", f"/disk/{disk_id}/artifact/ls", params=params or None)
220
+ data = await self._requester.request(
221
+ "GET", f"/disk/{disk_id}/artifact/ls", params=params or None
222
+ )
202
223
  return ListArtifactsResp.model_validate(data)
203
224
 
225
+ async def grep_artifacts(
226
+ self,
227
+ disk_id: str,
228
+ *,
229
+ query: str,
230
+ limit: int = 100,
231
+ ) -> list[Artifact]:
232
+ """Search artifact content using regex pattern.
233
+
234
+ Args:
235
+ disk_id: The disk ID to search in
236
+ query: Regex pattern to search for in file content
237
+ limit: Maximum number of results (default 100, max 1000)
238
+
239
+ Returns:
240
+ List of matching artifacts
241
+
242
+ Example:
243
+ ```python
244
+ # Search for TODO comments in code
245
+ results = await client.disks.artifacts.grep_artifacts(
246
+ disk_id="disk-uuid",
247
+ query="TODO.*bug"
248
+ )
249
+ ```
250
+ """
251
+ params = build_params(query=query, limit=limit)
252
+ data = await self._requester.request(
253
+ "GET", f"/disk/{disk_id}/artifact/grep", params=params
254
+ )
255
+ return [Artifact.model_validate(item) for item in data]
256
+
257
+ async def glob_artifacts(
258
+ self,
259
+ disk_id: str,
260
+ *,
261
+ query: str,
262
+ limit: int = 100,
263
+ ) -> list[Artifact]:
264
+ """Search artifact paths using glob pattern.
265
+
266
+ Args:
267
+ disk_id: The disk ID to search in
268
+ query: Glob pattern (e.g., '**/*.py', '*.txt')
269
+ limit: Maximum number of results (default 100, max 1000)
270
+
271
+ Returns:
272
+ List of matching artifacts
273
+
274
+ Example:
275
+ ```python
276
+ # Find all Python files
277
+ results = await client.disks.artifacts.glob_artifacts(
278
+ disk_id="disk-uuid",
279
+ query="**/*.py"
280
+ )
281
+ ```
282
+ """
283
+ params = build_params(query=query, limit=limit)
284
+ data = await self._requester.request(
285
+ "GET", f"/disk/{disk_id}/artifact/glob", params=params
286
+ )
287
+ return [Artifact.model_validate(item) for item in data]
@@ -78,6 +78,7 @@ class AsyncSessionsAPI:
78
78
  *,
79
79
  user: str | None = None,
80
80
  space_id: str | None = None,
81
+ disable_task_tracking: bool | None = None,
81
82
  configs: Mapping[str, Any] | None = None,
82
83
  ) -> Session:
83
84
  """Create a new session.
@@ -85,6 +86,7 @@ class AsyncSessionsAPI:
85
86
  Args:
86
87
  user: Optional user identifier string. Defaults to None.
87
88
  space_id: Optional space ID to associate with the session. Defaults to None.
89
+ disable_task_tracking: Whether to disable task tracking for this session. Defaults to None (server default: False).
88
90
  configs: Optional session configuration dictionary. Defaults to None.
89
91
 
90
92
  Returns:
@@ -95,6 +97,8 @@ class AsyncSessionsAPI:
95
97
  payload["user"] = user
96
98
  if space_id:
97
99
  payload["space_id"] = space_id
100
+ if disable_task_tracking is not None:
101
+ payload["disable_task_tracking"] = disable_task_tracking
98
102
  if configs is not None:
99
103
  payload["configs"] = configs
100
104
  data = await self._requester.request("POST", "/session", json_data=payload)
@@ -367,21 +371,22 @@ class AsyncSessionsAPI:
367
371
  )
368
372
  return TokenCounts.model_validate(data)
369
373
 
374
+ async def messages_observing_status(
375
+ self, session_id: str
376
+ ) -> MessageObservingStatus:
377
+ """Get message observing status counts for a session.
370
378
 
371
- async def messages_observing_status(self, session_id: str) -> MessageObservingStatus:
372
- """Get message observing status counts for a session.
379
+ Returns the count of messages by their observing status:
380
+ observed, in_process, and pending.
373
381
 
374
- Returns the count of messages by their observing status:
375
- observed, in_process, and pending.
376
-
377
- Args:
378
- session_id: The UUID of the session.
382
+ Args:
383
+ session_id: The UUID of the session.
379
384
 
380
- Returns:
381
- MessageObservingStatus object containing observed, in_process,
382
- pending counts and updated_at timestamp.
383
- """
384
- data = await self._requester.request(
385
- "GET", f"/session/{session_id}/observing_status"
386
- )
387
- return MessageObservingStatus.model_validate(data)
385
+ Returns:
386
+ MessageObservingStatus object containing observed, in_process,
387
+ pending counts and updated_at timestamp.
388
+ """
389
+ data = await self._requester.request(
390
+ "GET", f"/session/{session_id}/observing_status"
391
+ )
392
+ return MessageObservingStatus.model_validate(data)