lm-deluge 0.0.67__py3-none-any.whl → 0.0.88__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.

Potentially problematic release.


This version of lm-deluge might be problematic. Click here for more details.

Files changed (92) hide show
  1. lm_deluge/__init__.py +25 -2
  2. lm_deluge/api_requests/anthropic.py +92 -17
  3. lm_deluge/api_requests/base.py +47 -11
  4. lm_deluge/api_requests/bedrock.py +7 -4
  5. lm_deluge/api_requests/chat_reasoning.py +4 -0
  6. lm_deluge/api_requests/gemini.py +138 -18
  7. lm_deluge/api_requests/openai.py +114 -21
  8. lm_deluge/client.py +282 -49
  9. lm_deluge/config.py +15 -3
  10. lm_deluge/mock_openai.py +643 -0
  11. lm_deluge/models/__init__.py +12 -1
  12. lm_deluge/models/anthropic.py +17 -2
  13. lm_deluge/models/arcee.py +16 -0
  14. lm_deluge/models/deepseek.py +36 -4
  15. lm_deluge/models/google.py +29 -0
  16. lm_deluge/models/grok.py +24 -0
  17. lm_deluge/models/kimi.py +36 -0
  18. lm_deluge/models/minimax.py +10 -0
  19. lm_deluge/models/openai.py +100 -0
  20. lm_deluge/models/openrouter.py +86 -8
  21. lm_deluge/models/together.py +11 -0
  22. lm_deluge/models/zai.py +1 -0
  23. lm_deluge/pipelines/gepa/__init__.py +95 -0
  24. lm_deluge/pipelines/gepa/core.py +354 -0
  25. lm_deluge/pipelines/gepa/docs/samples.py +696 -0
  26. lm_deluge/pipelines/gepa/examples/01_synthetic_keywords.py +140 -0
  27. lm_deluge/pipelines/gepa/examples/02_gsm8k_math.py +261 -0
  28. lm_deluge/pipelines/gepa/examples/03_hotpotqa_multihop.py +300 -0
  29. lm_deluge/pipelines/gepa/examples/04_batch_classification.py +271 -0
  30. lm_deluge/pipelines/gepa/examples/simple_qa.py +129 -0
  31. lm_deluge/pipelines/gepa/optimizer.py +435 -0
  32. lm_deluge/pipelines/gepa/proposer.py +235 -0
  33. lm_deluge/pipelines/gepa/util.py +165 -0
  34. lm_deluge/{llm_tools → pipelines}/score.py +2 -2
  35. lm_deluge/{llm_tools → pipelines}/translate.py +5 -3
  36. lm_deluge/prompt.py +224 -40
  37. lm_deluge/request_context.py +7 -2
  38. lm_deluge/tool/__init__.py +1118 -0
  39. lm_deluge/tool/builtin/anthropic/__init__.py +300 -0
  40. lm_deluge/tool/builtin/gemini.py +59 -0
  41. lm_deluge/tool/builtin/openai.py +74 -0
  42. lm_deluge/tool/cua/__init__.py +173 -0
  43. lm_deluge/tool/cua/actions.py +148 -0
  44. lm_deluge/tool/cua/base.py +27 -0
  45. lm_deluge/tool/cua/batch.py +215 -0
  46. lm_deluge/tool/cua/converters.py +466 -0
  47. lm_deluge/tool/cua/kernel.py +702 -0
  48. lm_deluge/tool/cua/trycua.py +989 -0
  49. lm_deluge/tool/prefab/__init__.py +45 -0
  50. lm_deluge/tool/prefab/batch_tool.py +156 -0
  51. lm_deluge/tool/prefab/docs.py +1119 -0
  52. lm_deluge/tool/prefab/email.py +294 -0
  53. lm_deluge/tool/prefab/filesystem.py +1711 -0
  54. lm_deluge/tool/prefab/full_text_search/__init__.py +285 -0
  55. lm_deluge/tool/prefab/full_text_search/tantivy_index.py +396 -0
  56. lm_deluge/tool/prefab/memory.py +458 -0
  57. lm_deluge/tool/prefab/otc/__init__.py +165 -0
  58. lm_deluge/tool/prefab/otc/executor.py +281 -0
  59. lm_deluge/tool/prefab/otc/parse.py +188 -0
  60. lm_deluge/tool/prefab/random.py +212 -0
  61. lm_deluge/tool/prefab/rlm/__init__.py +296 -0
  62. lm_deluge/tool/prefab/rlm/executor.py +349 -0
  63. lm_deluge/tool/prefab/rlm/parse.py +144 -0
  64. lm_deluge/tool/prefab/sandbox.py +1621 -0
  65. lm_deluge/tool/prefab/sheets.py +385 -0
  66. lm_deluge/tool/prefab/subagents.py +233 -0
  67. lm_deluge/tool/prefab/todos.py +342 -0
  68. lm_deluge/tool/prefab/tool_search.py +169 -0
  69. lm_deluge/tool/prefab/web_search.py +199 -0
  70. lm_deluge/tracker.py +16 -13
  71. lm_deluge/util/schema.py +412 -0
  72. lm_deluge/warnings.py +8 -0
  73. {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.88.dist-info}/METADATA +22 -9
  74. lm_deluge-0.0.88.dist-info/RECORD +117 -0
  75. lm_deluge/built_in_tools/anthropic/__init__.py +0 -128
  76. lm_deluge/built_in_tools/openai.py +0 -28
  77. lm_deluge/presets/cerebras.py +0 -17
  78. lm_deluge/presets/meta.py +0 -13
  79. lm_deluge/tool.py +0 -849
  80. lm_deluge-0.0.67.dist-info/RECORD +0 -72
  81. lm_deluge/{llm_tools → pipelines}/__init__.py +1 -1
  82. /lm_deluge/{llm_tools → pipelines}/classify.py +0 -0
  83. /lm_deluge/{llm_tools → pipelines}/extract.py +0 -0
  84. /lm_deluge/{llm_tools → pipelines}/locate.py +0 -0
  85. /lm_deluge/{llm_tools → pipelines}/ocr.py +0 -0
  86. /lm_deluge/{built_in_tools → tool/builtin}/anthropic/bash.py +0 -0
  87. /lm_deluge/{built_in_tools → tool/builtin}/anthropic/computer_use.py +0 -0
  88. /lm_deluge/{built_in_tools → tool/builtin}/anthropic/editor.py +0 -0
  89. /lm_deluge/{built_in_tools → tool/builtin}/base.py +0 -0
  90. {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.88.dist-info}/WHEEL +0 -0
  91. {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.88.dist-info}/licenses/LICENSE +0 -0
  92. {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.88.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,385 @@
1
+ """Google Sheets manipulation prefab tool."""
2
+
3
+ import json
4
+ import os
5
+ from typing import Any
6
+
7
+ from lm_deluge.tool import Tool
8
+
9
+
10
+ class SheetsManager:
11
+ """
12
+ A prefab tool for manipulating Google Sheets.
13
+
14
+ Provides tools to read ranges and update individual cells in a Google Sheet.
15
+ Outputs are formatted as LLM-friendly HTML tables with row/col attributes.
16
+
17
+ Args:
18
+ sheet_id: The ID of the Google Sheet to manipulate
19
+ credentials_json: Optional. JSON string or dict containing Google service account credentials.
20
+ If not provided, will look for GOOGLE_SHEETS_CREDENTIALS env variable.
21
+ credentials_file: Optional. Path to a JSON file containing credentials.
22
+ Only used if credentials_json is not provided.
23
+ read_tool_name: Name for the read range tool (default: "sheets_read_range")
24
+ update_tool_name: Name for the update cell tool (default: "sheets_update_cell")
25
+
26
+ Example:
27
+ ```python
28
+ # Using credentials from environment
29
+ manager = SheetsManager(sheet_id="your-sheet-id-here")
30
+
31
+ # Using credentials directly
32
+ manager = SheetsManager(
33
+ sheet_id="your-sheet-id-here",
34
+ credentials_json={"type": "service_account", ...}
35
+ )
36
+
37
+ # Get tools
38
+ tools = manager.get_tools()
39
+ ```
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ sheet_id: str,
45
+ *,
46
+ credentials_json: str | dict[str, Any] | None = None,
47
+ credentials_file: str | None = None,
48
+ list_sheets_tool_name: str = "sheets_list_sheets",
49
+ get_used_range_tool_name: str = "sheets_get_used_range",
50
+ read_tool_name: str = "sheets_read_range",
51
+ update_tool_name: str = "sheets_update_cell",
52
+ ):
53
+ self.sheet_id = sheet_id
54
+ self.list_sheets_tool_name = list_sheets_tool_name
55
+ self.get_used_range_tool_name = get_used_range_tool_name
56
+ self.read_tool_name = read_tool_name
57
+ self.update_tool_name = update_tool_name
58
+
59
+ # Handle credentials
60
+ if credentials_json is not None:
61
+ if isinstance(credentials_json, str):
62
+ self.credentials = json.loads(credentials_json)
63
+ else:
64
+ self.credentials = credentials_json
65
+ elif credentials_file is not None:
66
+ with open(credentials_file, "r") as f:
67
+ self.credentials = json.load(f)
68
+ else:
69
+ # Try to load from environment
70
+ env_creds = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
71
+ if env_creds:
72
+ self.credentials = json.loads(env_creds)
73
+ else:
74
+ raise ValueError(
75
+ "No credentials provided. Please provide credentials_json, "
76
+ "credentials_file, or set GOOGLE_SHEETS_CREDENTIALS environment variable."
77
+ )
78
+
79
+ self._service = None
80
+ self._tools: list[Tool] | None = None
81
+
82
+ def _get_service(self):
83
+ """Lazily initialize the Google Sheets API service."""
84
+ if self._service is not None:
85
+ return self._service
86
+
87
+ try:
88
+ from google.oauth2 import service_account
89
+ from googleapiclient.discovery import build
90
+ except ImportError:
91
+ raise ImportError(
92
+ "Google Sheets API dependencies not installed. "
93
+ "Please install with: pip install google-api-python-client google-auth"
94
+ )
95
+
96
+ # Create credentials from service account info
97
+ creds = service_account.Credentials.from_service_account_info(
98
+ self.credentials, scopes=["https://www.googleapis.com/auth/spreadsheets"]
99
+ )
100
+
101
+ # Build the service
102
+ self._service = build("sheets", "v4", credentials=creds)
103
+ return self._service
104
+
105
+ def _list_sheets(self) -> str:
106
+ """
107
+ List all sheets (tabs) in the spreadsheet.
108
+
109
+ Returns:
110
+ JSON string with status and list of sheet names
111
+ """
112
+ try:
113
+ service = self._get_service()
114
+ spreadsheet = (
115
+ service.spreadsheets()
116
+ .get(spreadsheetId=self.sheet_id, fields="sheets.properties")
117
+ .execute()
118
+ )
119
+
120
+ sheets = []
121
+ for sheet in spreadsheet.get("sheets", []):
122
+ props = sheet.get("properties", {})
123
+ sheets.append(
124
+ {
125
+ "name": props.get("title", ""),
126
+ "index": props.get("index", 0),
127
+ "sheetId": props.get("sheetId", 0),
128
+ }
129
+ )
130
+
131
+ return json.dumps({"status": "success", "sheets": sheets})
132
+
133
+ except Exception as e:
134
+ return json.dumps({"status": "error", "error": str(e)})
135
+
136
+ def _get_used_range(self, sheet_name: str | None = None) -> str:
137
+ """
138
+ Get the used range of a sheet (the bounding box of all non-empty cells).
139
+
140
+ Args:
141
+ sheet_name: Name of the sheet to check. If None, uses the first sheet.
142
+
143
+ Returns:
144
+ JSON string with status and the used range in A1 notation
145
+ """
146
+ try:
147
+ service = self._get_service()
148
+
149
+ # If no sheet name provided, get the first sheet's name
150
+ if not sheet_name:
151
+ spreadsheet = (
152
+ service.spreadsheets()
153
+ .get(spreadsheetId=self.sheet_id, fields="sheets.properties.title")
154
+ .execute()
155
+ )
156
+ sheets = spreadsheet.get("sheets", [])
157
+ if not sheets:
158
+ return json.dumps(
159
+ {"status": "error", "error": "No sheets found in spreadsheet"}
160
+ )
161
+ sheet_name = sheets[0]["properties"]["title"]
162
+
163
+ # Get all values from the sheet to determine the used range
164
+ result = (
165
+ service.spreadsheets()
166
+ .values()
167
+ .get(spreadsheetId=self.sheet_id, range=f"'{sheet_name}'")
168
+ .execute()
169
+ )
170
+
171
+ values = result.get("values", [])
172
+
173
+ if not values:
174
+ return json.dumps(
175
+ {
176
+ "status": "success",
177
+ "sheet_name": sheet_name,
178
+ "used_range": None,
179
+ "message": "Sheet is empty",
180
+ }
181
+ )
182
+
183
+ # Find the max column across all rows
184
+ max_col = 0
185
+ for row in values:
186
+ if row: # Skip empty rows
187
+ max_col = max(max_col, len(row))
188
+
189
+ num_rows = len(values)
190
+ end_col = self._col_num_to_letter(max_col)
191
+ used_range = f"A1:{end_col}{num_rows}"
192
+
193
+ return json.dumps(
194
+ {
195
+ "status": "success",
196
+ "sheet_name": sheet_name,
197
+ "used_range": f"'{sheet_name}'!{used_range}",
198
+ "rows": num_rows,
199
+ "cols": max_col,
200
+ }
201
+ )
202
+
203
+ except Exception as e:
204
+ return json.dumps({"status": "error", "error": str(e)})
205
+
206
+ def _read_range(self, range_spec: str) -> str:
207
+ """
208
+ Read a range from the Google Sheet and return as HTML table.
209
+
210
+ Args:
211
+ range_spec: A1 notation range (e.g., "Sheet1!A1:C10" or just "A1:C10")
212
+
213
+ Returns:
214
+ JSON string with status and HTML table data
215
+ """
216
+ try:
217
+ service = self._get_service()
218
+ sheet = service.spreadsheets()
219
+
220
+ result = (
221
+ sheet.values()
222
+ .get(spreadsheetId=self.sheet_id, range=range_spec)
223
+ .execute()
224
+ )
225
+
226
+ values = result.get("values", [])
227
+
228
+ if not values:
229
+ return json.dumps(
230
+ {
231
+ "status": "success",
232
+ "message": "No data found in range",
233
+ "html": "<p>No data found</p>",
234
+ }
235
+ )
236
+
237
+ # Convert to HTML table with cell attribute for reference
238
+ html_parts = ["<table>"]
239
+
240
+ for row_idx, row in enumerate(values, start=1):
241
+ html_parts.append("<tr>")
242
+ for col_idx, cell_value in enumerate(row, start=1):
243
+ # Convert column index to letter (1=A, 2=B, etc.)
244
+ col_letter = self._col_num_to_letter(col_idx)
245
+ cell_ref = f"{col_letter}{row_idx}"
246
+
247
+ html_parts.append(f'<td cell="{cell_ref}">{cell_value}</td>')
248
+ html_parts.append("</tr>")
249
+
250
+ html_parts.append("</table>")
251
+ html_table = "\n".join(html_parts)
252
+
253
+ return json.dumps(
254
+ {"status": "success", "rows": len(values), "html": html_table}
255
+ )
256
+
257
+ except Exception as e:
258
+ return json.dumps({"status": "error", "error": str(e)})
259
+
260
+ def _update_cell(self, cell: str, value: str) -> str:
261
+ """
262
+ Update a single cell in the Google Sheet.
263
+
264
+ Args:
265
+ cell: Cell reference in A1 notation (e.g., "A1", "B5", "Sheet1!C3")
266
+ value: The value to set in the cell
267
+
268
+ Returns:
269
+ JSON string with status and result
270
+ """
271
+ try:
272
+ service = self._get_service()
273
+ sheet = service.spreadsheets()
274
+
275
+ body = {"values": [[value]]}
276
+
277
+ result = (
278
+ sheet.values()
279
+ .update(
280
+ spreadsheetId=self.sheet_id,
281
+ range=cell,
282
+ valueInputOption="USER_ENTERED", # Parse values like formulas, numbers, dates
283
+ body=body,
284
+ )
285
+ .execute()
286
+ )
287
+
288
+ return json.dumps(
289
+ {
290
+ "status": "success",
291
+ "updated_cells": result.get("updatedCells", 0),
292
+ "updated_range": result.get("updatedRange", ""),
293
+ "message": f"Successfully updated {cell} to '{value}'",
294
+ }
295
+ )
296
+
297
+ except Exception as e:
298
+ return json.dumps({"status": "error", "error": str(e)})
299
+
300
+ @staticmethod
301
+ def _col_num_to_letter(n: int) -> str:
302
+ """Convert column number to letter (1=A, 2=B, ..., 26=Z, 27=AA, etc.)."""
303
+ result = ""
304
+ while n > 0:
305
+ n -= 1
306
+ result = chr(65 + (n % 26)) + result
307
+ n //= 26
308
+ return result
309
+
310
+ def get_tools(self) -> list[Tool]:
311
+ """Return the list of Google Sheets tools."""
312
+ if self._tools is not None:
313
+ return self._tools
314
+
315
+ self._tools = [
316
+ Tool(
317
+ name=self.list_sheets_tool_name,
318
+ description=(
319
+ "List all sheets (tabs) in the spreadsheet. Returns the name, index, "
320
+ "and ID of each sheet."
321
+ ),
322
+ run=self._list_sheets,
323
+ parameters={},
324
+ required=[],
325
+ ),
326
+ Tool(
327
+ name=self.get_used_range_tool_name,
328
+ description=(
329
+ "Get the used range of a sheet (the bounding box containing all non-empty cells). "
330
+ "Returns the range in A1 notation (e.g., 'Sheet1'!A1:C4)."
331
+ ),
332
+ run=self._get_used_range,
333
+ parameters={
334
+ "sheet_name": {
335
+ "type": "string",
336
+ "description": (
337
+ "Name of the sheet to check. If not provided, uses the first sheet."
338
+ ),
339
+ }
340
+ },
341
+ required=[],
342
+ ),
343
+ Tool(
344
+ name=self.read_tool_name,
345
+ description=(
346
+ "Read a range of cells from the Google Sheet and return as an HTML table. "
347
+ "Each cell has a 'cell' attribute with its A1 reference (e.g., cell='A1'). "
348
+ "Use A1 notation for the range (e.g., 'A1:C10' or 'Sheet1!A1:C10')."
349
+ ),
350
+ run=self._read_range,
351
+ parameters={
352
+ "range_spec": {
353
+ "type": "string",
354
+ "description": (
355
+ "The range to read in A1 notation. Examples: 'A1:C10', 'Sheet1!A1:C10', "
356
+ "'A:A' (entire column A), '1:1' (entire row 1)"
357
+ ),
358
+ }
359
+ },
360
+ required=["range_spec"],
361
+ ),
362
+ Tool(
363
+ name=self.update_tool_name,
364
+ description=(
365
+ "Update a single cell in the Google Sheet. The value will be parsed "
366
+ "automatically (formulas, numbers, dates, etc.)."
367
+ ),
368
+ run=self._update_cell,
369
+ parameters={
370
+ "cell": {
371
+ "type": "string",
372
+ "description": (
373
+ "The cell to update in A1 notation. Examples: 'A1', 'B5', 'Sheet1!C3'"
374
+ ),
375
+ },
376
+ "value": {
377
+ "type": "string",
378
+ "description": "The value to set in the cell",
379
+ },
380
+ },
381
+ required=["cell", "value"],
382
+ ),
383
+ ]
384
+
385
+ return self._tools
@@ -0,0 +1,233 @@
1
+ from lm_deluge.api_requests.base import APIResponse
2
+ from lm_deluge.client import AgentLoopResponse, _LLMClient
3
+ from lm_deluge.prompt import Conversation, prompts_to_conversations
4
+ from lm_deluge.tool import Tool
5
+
6
+
7
+ class SubAgentManager:
8
+ """Manages subagent tasks that can be spawned by a main LLM via tool calls.
9
+
10
+ The SubAgentManager exposes tools that allow a main LLM to delegate subtasks
11
+ to specialized or cheaper subagent models, saving context and improving efficiency.
12
+
13
+ Example:
14
+ >>> manager = SubAgentManager(
15
+ ... client=LLMClient("gpt-4o-mini"), # Subagent model
16
+ ... tools=[search_tool, calculator_tool] # Tools available to subagents
17
+ ... )
18
+ >>> main_client = LLMClient("gpt-4o") # More expensive main model
19
+ >>> conv = Conversation.user("Research AI and calculate market size")
20
+ >>> # Main model can now call manager tools to spawn subagents
21
+ >>> conv, resp = await main_client.run_agent_loop(
22
+ ... conv,
23
+ ... tools=manager.get_tools()
24
+ ... )
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ client: _LLMClient,
30
+ tools: list[Tool] | None = None,
31
+ max_rounds: int = 5,
32
+ ):
33
+ """Initialize the SubAgentManager.
34
+
35
+ Args:
36
+ client: LLMClient to use for subagent tasks
37
+ tools: Tools available to subagents (optional)
38
+ max_rounds: Maximum rounds for each subagent's agent loop
39
+ """
40
+ self.client = client
41
+ self.tools = tools or []
42
+ self.max_rounds = max_rounds
43
+ self.subagents: dict[int, dict] = {}
44
+
45
+ async def _start_subagent(self, task: str) -> int:
46
+ """Start a subagent with the given task.
47
+
48
+ Args:
49
+ task: The task description for the subagent
50
+
51
+ Returns:
52
+ Subagent task ID
53
+ """
54
+ conversation = prompts_to_conversations([task])[0]
55
+ assert isinstance(conversation, Conversation)
56
+
57
+ # Use agent loop nowait API to start the subagent
58
+ task_id = self.client.start_agent_loop_nowait(
59
+ conversation,
60
+ tools=self.tools, # type: ignore
61
+ max_rounds=self.max_rounds,
62
+ )
63
+
64
+ # Track the subagent
65
+ self.subagents[task_id] = {
66
+ "status": "running",
67
+ "conversation": None,
68
+ "response": None,
69
+ "error": None,
70
+ }
71
+
72
+ return task_id
73
+
74
+ def _finalize_subagent_result(
75
+ self, agent_id: int, result: AgentLoopResponse
76
+ ) -> str:
77
+ """Update subagent tracking state from a finished agent loop."""
78
+ agent = self.subagents[agent_id]
79
+ agent["conversation"] = result.conversation
80
+ agent["response"] = result.final_response
81
+
82
+ if result.final_response.is_error:
83
+ agent["status"] = "error"
84
+ agent["error"] = result.final_response.error_message
85
+ return f"Error: {agent['error']}"
86
+
87
+ agent["status"] = "finished"
88
+ return result.final_response.completion or "Subagent finished with no output"
89
+
90
+ async def _check_subagent(self, agent_id: int) -> str:
91
+ """Check the status of a subagent.
92
+
93
+ Args:
94
+ agent_id: The subagent task ID
95
+
96
+ Returns:
97
+ Status string describing the subagent's state
98
+ """
99
+ if agent_id not in self.subagents:
100
+ return f"Error: Subagent {agent_id} not found"
101
+
102
+ agent = self.subagents[agent_id]
103
+ status = agent["status"]
104
+
105
+ if status == "finished":
106
+ response: APIResponse = agent["response"]
107
+ return response.completion or "Subagent finished with no output"
108
+ elif status == "error":
109
+ return f"Error: {agent['error']}"
110
+ else:
111
+ # Try to check if it's done
112
+ try:
113
+ # Check if the task exists in client's results
114
+ stored_result = self.client._results.get(agent_id)
115
+ if isinstance(stored_result, AgentLoopResponse):
116
+ return self._finalize_subagent_result(agent_id, stored_result)
117
+
118
+ task = self.client._tasks.get(agent_id)
119
+ if task and task.done():
120
+ try:
121
+ task_result = task.result()
122
+ except Exception as e:
123
+ agent["status"] = "error"
124
+ agent["error"] = str(e)
125
+ return f"Error: {agent['error']}"
126
+
127
+ if isinstance(task_result, AgentLoopResponse):
128
+ return self._finalize_subagent_result(agent_id, task_result)
129
+
130
+ agent["status"] = "error"
131
+ agent["error"] = (
132
+ f"Unexpected task result type: {type(task_result).__name__}"
133
+ )
134
+ return f"Error: {agent['error']}"
135
+
136
+ # Still running
137
+ return f"Subagent {agent_id} is still running. Call this tool again to check status."
138
+ except Exception as e:
139
+ agent["status"] = "error"
140
+ agent["error"] = str(e)
141
+ return f"Error checking subagent: {e}"
142
+
143
+ async def _wait_for_subagent(self, agent_id: int) -> str:
144
+ """Wait for a subagent to complete and return its output.
145
+
146
+ Args:
147
+ agent_id: The subagent task ID
148
+
149
+ Returns:
150
+ The subagent's final output
151
+ """
152
+ if agent_id not in self.subagents:
153
+ return f"Error: Subagent {agent_id} not found"
154
+
155
+ try:
156
+ # Use the wait_for_agent_loop API
157
+ conversation, response = await self.client.wait_for_agent_loop(agent_id)
158
+
159
+ agent = self.subagents[agent_id]
160
+ agent["conversation"] = conversation
161
+ agent["response"] = response
162
+
163
+ if response.is_error:
164
+ agent["status"] = "error"
165
+ agent["error"] = response.error_message
166
+ return f"Error: {response.error_message}"
167
+ else:
168
+ agent["status"] = "finished"
169
+ return response.completion or "Subagent finished with no output"
170
+ except Exception as e:
171
+ agent = self.subagents[agent_id]
172
+ agent["status"] = "error"
173
+ agent["error"] = str(e)
174
+ return f"Error waiting for subagent: {e}"
175
+
176
+ def get_tools(self) -> list[Tool]:
177
+ """Get the tools that allow a main LLM to control subagents.
178
+
179
+ Returns:
180
+ List of Tool objects for starting, checking, and waiting for subagents
181
+ """
182
+ start_tool = Tool(
183
+ name="start_subagent",
184
+ description=(
185
+ "Start a subagent to work on a subtask independently. "
186
+ "Use this to delegate complex subtasks or when you need to save context. "
187
+ "Returns the subagent's task ID which can be used to check its status."
188
+ ),
189
+ run=self._start_subagent,
190
+ parameters={
191
+ "task": {
192
+ "type": "string",
193
+ "description": "The task description for the subagent to work on",
194
+ }
195
+ },
196
+ required=["task"],
197
+ )
198
+
199
+ check_tool = Tool(
200
+ name="check_subagent",
201
+ description=(
202
+ "Check the status and output of a running subagent. "
203
+ "If the subagent is still running, you'll be told to check again later. "
204
+ "If finished, returns the subagent's final output."
205
+ ),
206
+ run=self._check_subagent,
207
+ parameters={
208
+ "agent_id": {
209
+ "type": "integer",
210
+ "description": "The task ID of the subagent to check",
211
+ }
212
+ },
213
+ required=["agent_id"],
214
+ )
215
+
216
+ wait_tool = Tool(
217
+ name="wait_for_subagent",
218
+ description=(
219
+ "Wait for a subagent to complete and return its output. "
220
+ "This will block until the subagent finishes. "
221
+ "Use check_subagent if you want to do other work while waiting."
222
+ ),
223
+ run=self._wait_for_subagent,
224
+ parameters={
225
+ "agent_id": {
226
+ "type": "integer",
227
+ "description": "The task ID of the subagent to wait for",
228
+ }
229
+ },
230
+ required=["agent_id"],
231
+ )
232
+
233
+ return [start_tool, check_tool, wait_tool]