lm-deluge 0.0.74__py3-none-any.whl → 0.0.76__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.
- lm_deluge/api_requests/anthropic.py +10 -1
- lm_deluge/api_requests/bedrock.py +1 -3
- lm_deluge/api_requests/openai.py +16 -2
- lm_deluge/client.py +107 -26
- lm_deluge/llm_tools/__init__.py +8 -1
- lm_deluge/llm_tools/filesystem.py +0 -0
- lm_deluge/llm_tools/subagents.py +233 -0
- lm_deluge/llm_tools/todos.py +342 -0
- lm_deluge/request_context.py +5 -2
- lm_deluge/util/schema.py +412 -0
- {lm_deluge-0.0.74.dist-info → lm_deluge-0.0.76.dist-info}/METADATA +1 -1
- {lm_deluge-0.0.74.dist-info → lm_deluge-0.0.76.dist-info}/RECORD +15 -11
- {lm_deluge-0.0.74.dist-info → lm_deluge-0.0.76.dist-info}/WHEEL +0 -0
- {lm_deluge-0.0.74.dist-info → lm_deluge-0.0.76.dist-info}/licenses/LICENSE +0 -0
- {lm_deluge-0.0.74.dist-info → lm_deluge-0.0.76.dist-info}/top_level.txt +0 -0
|
@@ -12,6 +12,10 @@ from lm_deluge.prompt import (
|
|
|
12
12
|
from lm_deluge.request_context import RequestContext
|
|
13
13
|
from lm_deluge.tool import MCPServer, Tool
|
|
14
14
|
from lm_deluge.usage import Usage
|
|
15
|
+
from lm_deluge.util.schema import (
|
|
16
|
+
prepare_output_schema,
|
|
17
|
+
transform_schema_for_anthropic,
|
|
18
|
+
)
|
|
15
19
|
|
|
16
20
|
from ..models import APIModel
|
|
17
21
|
from .base import APIRequestBase, APIResponse
|
|
@@ -87,10 +91,15 @@ def _build_anthropic_request(
|
|
|
87
91
|
# Handle structured outputs (output_format)
|
|
88
92
|
if context.output_schema:
|
|
89
93
|
if model.supports_json:
|
|
94
|
+
base_schema = prepare_output_schema(context.output_schema)
|
|
95
|
+
|
|
96
|
+
# Apply Anthropic-specific transformations (move unsupported constraints to description)
|
|
97
|
+
transformed_schema = transform_schema_for_anthropic(base_schema)
|
|
98
|
+
|
|
90
99
|
_add_beta(base_headers, "structured-outputs-2025-11-13")
|
|
91
100
|
request_json["output_format"] = {
|
|
92
101
|
"type": "json_schema",
|
|
93
|
-
"schema":
|
|
102
|
+
"schema": transformed_schema,
|
|
94
103
|
}
|
|
95
104
|
else:
|
|
96
105
|
print(
|
|
@@ -197,9 +197,7 @@ async def _build_openai_bedrock_request(
|
|
|
197
197
|
request_tools = []
|
|
198
198
|
for tool in tools:
|
|
199
199
|
if isinstance(tool, Tool):
|
|
200
|
-
request_tools.append(
|
|
201
|
-
tool.dump_for("openai-completions", strict=False)
|
|
202
|
-
)
|
|
200
|
+
request_tools.append(tool.dump_for("openai-completions", strict=False))
|
|
203
201
|
elif isinstance(tool, MCPServer):
|
|
204
202
|
as_tools = await tool.to_tools()
|
|
205
203
|
request_tools.extend(
|
lm_deluge/api_requests/openai.py
CHANGED
|
@@ -9,6 +9,10 @@ from aiohttp import ClientResponse
|
|
|
9
9
|
from lm_deluge.request_context import RequestContext
|
|
10
10
|
from lm_deluge.tool import MCPServer, Tool
|
|
11
11
|
from lm_deluge.warnings import maybe_warn
|
|
12
|
+
from lm_deluge.util.schema import (
|
|
13
|
+
prepare_output_schema,
|
|
14
|
+
transform_schema_for_openai,
|
|
15
|
+
)
|
|
12
16
|
|
|
13
17
|
from ..config import SamplingParams
|
|
14
18
|
from ..models import APIModel
|
|
@@ -87,11 +91,16 @@ async def _build_oa_chat_request(
|
|
|
87
91
|
# Handle structured outputs (output_schema takes precedence over json_mode)
|
|
88
92
|
if context.output_schema:
|
|
89
93
|
if model.supports_json:
|
|
94
|
+
base_schema = prepare_output_schema(context.output_schema)
|
|
95
|
+
|
|
96
|
+
# Apply OpenAI-specific transformations (currently passthrough with copy)
|
|
97
|
+
transformed_schema = transform_schema_for_openai(base_schema)
|
|
98
|
+
|
|
90
99
|
request_json["response_format"] = {
|
|
91
100
|
"type": "json_schema",
|
|
92
101
|
"json_schema": {
|
|
93
102
|
"name": "response",
|
|
94
|
-
"schema":
|
|
103
|
+
"schema": transformed_schema,
|
|
95
104
|
"strict": True,
|
|
96
105
|
},
|
|
97
106
|
}
|
|
@@ -326,11 +335,16 @@ async def _build_oa_responses_request(
|
|
|
326
335
|
# Handle structured outputs (output_schema takes precedence over json_mode)
|
|
327
336
|
if context.output_schema:
|
|
328
337
|
if model.supports_json:
|
|
338
|
+
base_schema = prepare_output_schema(context.output_schema)
|
|
339
|
+
|
|
340
|
+
# Apply OpenAI-specific transformations (currently passthrough with copy)
|
|
341
|
+
transformed_schema = transform_schema_for_openai(base_schema)
|
|
342
|
+
|
|
329
343
|
request_json["text"] = {
|
|
330
344
|
"format": {
|
|
331
345
|
"type": "json_schema",
|
|
332
346
|
"name": "response",
|
|
333
|
-
"schema":
|
|
347
|
+
"schema": transformed_schema,
|
|
334
348
|
"strict": True,
|
|
335
349
|
}
|
|
336
350
|
}
|
lm_deluge/client.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from typing import (
|
|
3
4
|
Any,
|
|
4
5
|
AsyncGenerator,
|
|
@@ -37,6 +38,14 @@ from .request_context import RequestContext
|
|
|
37
38
|
from .tracker import StatusTracker
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
@dataclass
|
|
42
|
+
class AgentLoopResponse:
|
|
43
|
+
"""Wrapper for agent loop results to distinguish from single request results."""
|
|
44
|
+
|
|
45
|
+
conversation: Conversation
|
|
46
|
+
final_response: APIResponse
|
|
47
|
+
|
|
48
|
+
|
|
40
49
|
# TODO: add optional max_input_tokens to client so we can reject long prompts to prevent abuse
|
|
41
50
|
class _LLMClient(BaseModel):
|
|
42
51
|
"""
|
|
@@ -88,7 +97,9 @@ class _LLMClient(BaseModel):
|
|
|
88
97
|
# Internal state for async task handling
|
|
89
98
|
_next_task_id: int = PrivateAttr(default=0)
|
|
90
99
|
_tasks: dict[int, asyncio.Task] = PrivateAttr(default_factory=dict)
|
|
91
|
-
_results: dict[int, APIResponse] = PrivateAttr(
|
|
100
|
+
_results: dict[int, APIResponse | AgentLoopResponse] = PrivateAttr(
|
|
101
|
+
default_factory=dict
|
|
102
|
+
)
|
|
92
103
|
_tracker: StatusTracker | None = PrivateAttr(default=None)
|
|
93
104
|
_capacity_lock: asyncio.Lock = PrivateAttr(default_factory=asyncio.Lock)
|
|
94
105
|
|
|
@@ -561,7 +572,7 @@ class _LLMClient(BaseModel):
|
|
|
561
572
|
return_completions_only: Literal[True],
|
|
562
573
|
show_progress: bool = ...,
|
|
563
574
|
tools: list[Tool | dict | MCPServer] | None = ...,
|
|
564
|
-
output_schema: dict | None = ...,
|
|
575
|
+
output_schema: type[BaseModel] | dict | None = ...,
|
|
565
576
|
cache: CachePattern | None = ...,
|
|
566
577
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = ...,
|
|
567
578
|
) -> list[str | None]: ...
|
|
@@ -574,7 +585,7 @@ class _LLMClient(BaseModel):
|
|
|
574
585
|
return_completions_only: Literal[False] = ...,
|
|
575
586
|
show_progress: bool = ...,
|
|
576
587
|
tools: list[Tool | dict | MCPServer] | None = ...,
|
|
577
|
-
output_schema: dict | None = ...,
|
|
588
|
+
output_schema: type[BaseModel] | dict | None = ...,
|
|
578
589
|
cache: CachePattern | None = ...,
|
|
579
590
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = ...,
|
|
580
591
|
) -> list[APIResponse]: ...
|
|
@@ -586,7 +597,7 @@ class _LLMClient(BaseModel):
|
|
|
586
597
|
return_completions_only: bool = False,
|
|
587
598
|
show_progress: bool = True,
|
|
588
599
|
tools: list[Tool | dict | MCPServer] | None = None,
|
|
589
|
-
output_schema: dict | None = None,
|
|
600
|
+
output_schema: type[BaseModel] | dict | None = None,
|
|
590
601
|
cache: CachePattern | None = None,
|
|
591
602
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = None,
|
|
592
603
|
) -> list[APIResponse] | list[str | None] | dict[str, int]:
|
|
@@ -661,7 +672,7 @@ class _LLMClient(BaseModel):
|
|
|
661
672
|
return_completions_only: bool = False,
|
|
662
673
|
show_progress=True,
|
|
663
674
|
tools: list[Tool | dict | MCPServer] | None = None,
|
|
664
|
-
output_schema: dict | None = None,
|
|
675
|
+
output_schema: type[BaseModel] | dict | None = None,
|
|
665
676
|
cache: CachePattern | None = None,
|
|
666
677
|
):
|
|
667
678
|
return asyncio.run(
|
|
@@ -694,7 +705,7 @@ class _LLMClient(BaseModel):
|
|
|
694
705
|
prompt: Prompt,
|
|
695
706
|
*,
|
|
696
707
|
tools: list[Tool | dict | MCPServer] | None = None,
|
|
697
|
-
output_schema: dict | None = None,
|
|
708
|
+
output_schema: type[BaseModel] | dict | None = None,
|
|
698
709
|
cache: CachePattern | None = None,
|
|
699
710
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = None,
|
|
700
711
|
) -> int:
|
|
@@ -731,7 +742,7 @@ class _LLMClient(BaseModel):
|
|
|
731
742
|
prompt: Prompt,
|
|
732
743
|
*,
|
|
733
744
|
tools: list[Tool | dict | MCPServer] | None = None,
|
|
734
|
-
output_schema: dict | None = None,
|
|
745
|
+
output_schema: type[BaseModel] | dict | None = None,
|
|
735
746
|
cache: CachePattern | None = None,
|
|
736
747
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = None,
|
|
737
748
|
) -> APIResponse:
|
|
@@ -747,11 +758,11 @@ class _LLMClient(BaseModel):
|
|
|
747
758
|
async def wait_for(self, task_id: int) -> APIResponse:
|
|
748
759
|
task = self._tasks.get(task_id)
|
|
749
760
|
if task:
|
|
750
|
-
|
|
751
|
-
res = self._results.get(task_id)
|
|
752
|
-
if res:
|
|
753
|
-
return res
|
|
761
|
+
result = await task
|
|
754
762
|
else:
|
|
763
|
+
result = self._results.get(task_id)
|
|
764
|
+
|
|
765
|
+
if result is None:
|
|
755
766
|
return APIResponse(
|
|
756
767
|
id=-1,
|
|
757
768
|
model_internal="",
|
|
@@ -762,6 +773,11 @@ class _LLMClient(BaseModel):
|
|
|
762
773
|
error_message="Task not found",
|
|
763
774
|
)
|
|
764
775
|
|
|
776
|
+
assert isinstance(
|
|
777
|
+
result, APIResponse
|
|
778
|
+
), f"Expected APIResponse, got {type(result)}. Use wait_for_agent_loop for agent loop tasks."
|
|
779
|
+
return result
|
|
780
|
+
|
|
765
781
|
async def wait_for_all(
|
|
766
782
|
self, task_ids: Sequence[int] | None = None
|
|
767
783
|
) -> list[APIResponse]:
|
|
@@ -797,6 +813,9 @@ class _LLMClient(BaseModel):
|
|
|
797
813
|
tid = tasks_map.pop(task)
|
|
798
814
|
task_result = self._results.get(tid, await task)
|
|
799
815
|
assert task_result
|
|
816
|
+
assert isinstance(
|
|
817
|
+
task_result, APIResponse
|
|
818
|
+
), f"Expected APIResponse, got {type(task_result)}. as_completed() only works with single requests, not agent loops."
|
|
800
819
|
yield tid, task_result
|
|
801
820
|
|
|
802
821
|
while tasks_map:
|
|
@@ -807,6 +826,9 @@ class _LLMClient(BaseModel):
|
|
|
807
826
|
tid = tasks_map.pop(task)
|
|
808
827
|
task_result = self._results.get(tid, await task)
|
|
809
828
|
assert task_result
|
|
829
|
+
assert isinstance(
|
|
830
|
+
task_result, APIResponse
|
|
831
|
+
), f"Expected APIResponse, got {type(task_result)}. as_completed() only works with single requests, not agent loops."
|
|
810
832
|
yield tid, task_result
|
|
811
833
|
|
|
812
834
|
async def stream(
|
|
@@ -828,24 +850,15 @@ class _LLMClient(BaseModel):
|
|
|
828
850
|
return self.postprocess(item)
|
|
829
851
|
return item
|
|
830
852
|
|
|
831
|
-
async def
|
|
853
|
+
async def _run_agent_loop_internal(
|
|
832
854
|
self,
|
|
833
|
-
|
|
855
|
+
task_id: int,
|
|
856
|
+
conversation: Conversation,
|
|
834
857
|
*,
|
|
835
858
|
tools: list[Tool | dict | MCPServer] | None = None,
|
|
836
859
|
max_rounds: int = 5,
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
"""Run a simple agent loop until no more tool calls are returned.
|
|
840
|
-
|
|
841
|
-
The provided ``conversation`` will be mutated and returned alongside the
|
|
842
|
-
final ``APIResponse`` from the model. ``tools`` may include ``Tool``
|
|
843
|
-
instances or built‑in tool dictionaries.
|
|
844
|
-
"""
|
|
845
|
-
|
|
846
|
-
if not isinstance(conversation, Conversation):
|
|
847
|
-
conversation = prompts_to_conversations([conversation])[0]
|
|
848
|
-
assert isinstance(conversation, Conversation)
|
|
860
|
+
) -> AgentLoopResponse:
|
|
861
|
+
"""Internal method to run agent loop and return wrapped result."""
|
|
849
862
|
|
|
850
863
|
# Expand MCPServer objects to their constituent tools for tool execution
|
|
851
864
|
expanded_tools: list[Tool] = []
|
|
@@ -898,7 +911,75 @@ class _LLMClient(BaseModel):
|
|
|
898
911
|
if response is None:
|
|
899
912
|
raise RuntimeError("model did not return a response")
|
|
900
913
|
|
|
901
|
-
|
|
914
|
+
result = AgentLoopResponse(conversation=conversation, final_response=response)
|
|
915
|
+
self._results[task_id] = result
|
|
916
|
+
return result
|
|
917
|
+
|
|
918
|
+
def start_agent_loop_nowait(
|
|
919
|
+
self,
|
|
920
|
+
conversation: Prompt,
|
|
921
|
+
*,
|
|
922
|
+
tools: list[Tool | dict | MCPServer] | None = None,
|
|
923
|
+
max_rounds: int = 5,
|
|
924
|
+
) -> int:
|
|
925
|
+
"""Start an agent loop without waiting for it to complete.
|
|
926
|
+
|
|
927
|
+
Returns a task_id that can be used with wait_for_agent_loop().
|
|
928
|
+
"""
|
|
929
|
+
if not isinstance(conversation, Conversation):
|
|
930
|
+
conversation = prompts_to_conversations([conversation])[0]
|
|
931
|
+
assert isinstance(conversation, Conversation)
|
|
932
|
+
|
|
933
|
+
task_id = self._next_task_id
|
|
934
|
+
self._next_task_id += 1
|
|
935
|
+
|
|
936
|
+
task = asyncio.create_task(
|
|
937
|
+
self._run_agent_loop_internal(
|
|
938
|
+
task_id, conversation, tools=tools, max_rounds=max_rounds
|
|
939
|
+
)
|
|
940
|
+
)
|
|
941
|
+
self._tasks[task_id] = task
|
|
942
|
+
return task_id
|
|
943
|
+
|
|
944
|
+
async def wait_for_agent_loop(
|
|
945
|
+
self, task_id: int
|
|
946
|
+
) -> tuple[Conversation, APIResponse]:
|
|
947
|
+
"""Wait for an agent loop task to complete.
|
|
948
|
+
|
|
949
|
+
Returns the conversation and final response from the agent loop.
|
|
950
|
+
"""
|
|
951
|
+
task = self._tasks.get(task_id)
|
|
952
|
+
if task:
|
|
953
|
+
result = await task
|
|
954
|
+
else:
|
|
955
|
+
result = self._results.get(task_id)
|
|
956
|
+
|
|
957
|
+
if result is None:
|
|
958
|
+
raise RuntimeError(f"Agent loop task {task_id} not found")
|
|
959
|
+
|
|
960
|
+
assert isinstance(
|
|
961
|
+
result, AgentLoopResponse
|
|
962
|
+
), f"Expected AgentLoopResponse, got {type(result)}"
|
|
963
|
+
return result.conversation, result.final_response
|
|
964
|
+
|
|
965
|
+
async def run_agent_loop(
|
|
966
|
+
self,
|
|
967
|
+
conversation: Prompt,
|
|
968
|
+
*,
|
|
969
|
+
tools: list[Tool | dict | MCPServer] | None = None,
|
|
970
|
+
max_rounds: int = 5,
|
|
971
|
+
show_progress: bool = False,
|
|
972
|
+
) -> tuple[Conversation, APIResponse]:
|
|
973
|
+
"""Run a simple agent loop until no more tool calls are returned.
|
|
974
|
+
|
|
975
|
+
The provided ``conversation`` will be mutated and returned alongside the
|
|
976
|
+
final ``APIResponse`` from the model. ``tools`` may include ``Tool``
|
|
977
|
+
instances or built‑in tool dictionaries.
|
|
978
|
+
"""
|
|
979
|
+
task_id = self.start_agent_loop_nowait(
|
|
980
|
+
conversation, tools=tools, max_rounds=max_rounds
|
|
981
|
+
)
|
|
982
|
+
return await self.wait_for_agent_loop(task_id)
|
|
902
983
|
|
|
903
984
|
def run_agent_loop_sync(
|
|
904
985
|
self,
|
lm_deluge/llm_tools/__init__.py
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
from .extract import extract, extract_async
|
|
2
|
-
from .translate import translate, translate_async
|
|
3
2
|
from .score import score_llm
|
|
3
|
+
from .subagents import SubAgentManager
|
|
4
|
+
from .todos import TodoItem, TodoManager, TodoPriority, TodoStatus
|
|
5
|
+
from .translate import translate, translate_async
|
|
4
6
|
|
|
5
7
|
__all__ = [
|
|
6
8
|
"extract",
|
|
7
9
|
"extract_async",
|
|
10
|
+
"TodoItem",
|
|
11
|
+
"TodoManager",
|
|
12
|
+
"TodoPriority",
|
|
13
|
+
"TodoStatus",
|
|
8
14
|
"translate",
|
|
9
15
|
"translate_async",
|
|
10
16
|
"score_llm",
|
|
17
|
+
"SubAgentManager",
|
|
11
18
|
]
|
|
File without changes
|
|
@@ -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]
|