kosong-x 0.52.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.
Files changed (35) hide show
  1. kosong_x-0.52.0/PKG-INFO +203 -0
  2. kosong_x-0.52.0/README.md +183 -0
  3. kosong_x-0.52.0/pyproject.toml +76 -0
  4. kosong_x-0.52.0/src/kosong/__init__.py +216 -0
  5. kosong_x-0.52.0/src/kosong/__main__.py +164 -0
  6. kosong_x-0.52.0/src/kosong/_generate.py +120 -0
  7. kosong_x-0.52.0/src/kosong/chat_provider/__init__.py +186 -0
  8. kosong_x-0.52.0/src/kosong/chat_provider/chaos.py +292 -0
  9. kosong_x-0.52.0/src/kosong/chat_provider/echo/__init__.py +9 -0
  10. kosong_x-0.52.0/src/kosong/chat_provider/echo/dsl.py +234 -0
  11. kosong_x-0.52.0/src/kosong/chat_provider/echo/echo.py +125 -0
  12. kosong_x-0.52.0/src/kosong/chat_provider/echo/scripted_echo.py +103 -0
  13. kosong_x-0.52.0/src/kosong/chat_provider/kimi.py +518 -0
  14. kosong_x-0.52.0/src/kosong/chat_provider/mock.py +80 -0
  15. kosong_x-0.52.0/src/kosong/chat_provider/openai_common.py +167 -0
  16. kosong_x-0.52.0/src/kosong/contrib/__init__.py +0 -0
  17. kosong_x-0.52.0/src/kosong/contrib/chat_provider/__init__.py +0 -0
  18. kosong_x-0.52.0/src/kosong/contrib/chat_provider/anthropic.py +723 -0
  19. kosong_x-0.52.0/src/kosong/contrib/chat_provider/common.py +5 -0
  20. kosong_x-0.52.0/src/kosong/contrib/chat_provider/google_genai.py +769 -0
  21. kosong_x-0.52.0/src/kosong/contrib/chat_provider/openai_legacy.py +370 -0
  22. kosong_x-0.52.0/src/kosong/contrib/chat_provider/openai_responses.py +602 -0
  23. kosong_x-0.52.0/src/kosong/contrib/context/__init__.py +0 -0
  24. kosong_x-0.52.0/src/kosong/contrib/context/linear.py +145 -0
  25. kosong_x-0.52.0/src/kosong/message.py +303 -0
  26. kosong_x-0.52.0/src/kosong/py.typed +0 -0
  27. kosong_x-0.52.0/src/kosong/tooling/__init__.py +354 -0
  28. kosong_x-0.52.0/src/kosong/tooling/empty.py +24 -0
  29. kosong_x-0.52.0/src/kosong/tooling/error.py +41 -0
  30. kosong_x-0.52.0/src/kosong/tooling/mcp.py +72 -0
  31. kosong_x-0.52.0/src/kosong/tooling/simple.py +133 -0
  32. kosong_x-0.52.0/src/kosong/utils/__init__.py +0 -0
  33. kosong_x-0.52.0/src/kosong/utils/aio.py +14 -0
  34. kosong_x-0.52.0/src/kosong/utils/jsonschema.py +250 -0
  35. kosong_x-0.52.0/src/kosong/utils/typing.py +3 -0
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.3
2
+ Name: kosong-x
3
+ Version: 0.52.0
4
+ Summary: The LLM abstraction layer for modern AI agent applications.
5
+ Requires-Dist: anthropic>=0.78.0
6
+ Requires-Dist: certifi>=2024.0.0
7
+ Requires-Dist: google-genai>=1.56.0
8
+ Requires-Dist: jsonschema>=4.25.1
9
+ Requires-Dist: loguru>=0.6.0,<0.8
10
+ Requires-Dist: openai>=2.14.0,<2.15.0
11
+ Requires-Dist: pydantic>=2.12.5
12
+ Requires-Dist: python-dotenv>=1.2.1
13
+ Requires-Dist: typing-extensions>=4.15.0
14
+ Requires-Dist: mcp>=1,<2
15
+ Requires-Dist: anthropic>=0.78.0 ; extra == 'contrib'
16
+ Requires-Dist: google-genai>=1.55.0 ; extra == 'contrib'
17
+ Requires-Python: >=3.12
18
+ Provides-Extra: contrib
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Kosong
22
+
23
+ Kosong is an LLM abstraction layer designed for modern AI agent applications. It unifies message structures, asynchronous tool orchestration, and pluggable chat providers so you can build agents with ease and avoid vendor lock-in.
24
+
25
+ > Kosong means "empty" in Malay and Indonesian.
26
+
27
+ ## Installation
28
+
29
+ Kosong requires Python 3.12 or higher. We recommend using uv as the package manager.
30
+
31
+ Init your project with:
32
+
33
+ ```bash
34
+ uv init --python 3.12 # or higher
35
+ ```
36
+
37
+ Then add Kosong as a dependency:
38
+
39
+ ```bash
40
+ uv add kosong
41
+ ```
42
+
43
+ To enable chat providers other than Kimi (e.g. Anthropic and Google Gemini), install the optional extra:
44
+
45
+ ```bash
46
+ uv add 'kosong[contrib]'
47
+ ```
48
+
49
+ ## Examples
50
+
51
+ ### Simple chat completion
52
+
53
+ ```python
54
+ import asyncio
55
+
56
+ import kosong
57
+ from kosong.chat_provider.kimi import Kimi
58
+ from kosong.message import Message
59
+
60
+
61
+ async def main() -> None:
62
+ kimi = Kimi(
63
+ base_url="https://api.moonshot.ai/v1",
64
+ api_key="your_kimi_api_key_here",
65
+ model="kimi-k2-turbo-preview",
66
+ )
67
+
68
+ history = [
69
+ Message(role="user", content="Who are you?"),
70
+ ]
71
+
72
+ result = await kosong.generate(
73
+ chat_provider=kimi,
74
+ system_prompt="You are a helpful assistant.",
75
+ tools=[],
76
+ history=history,
77
+ )
78
+ print(result.message)
79
+ print(result.usage)
80
+
81
+
82
+ asyncio.run(main())
83
+ ```
84
+
85
+ ### Streaming output
86
+
87
+ ```python
88
+ import asyncio
89
+
90
+ import kosong
91
+ from kosong.chat_provider import StreamedMessagePart
92
+ from kosong.chat_provider.kimi import Kimi
93
+ from kosong.message import Message
94
+
95
+
96
+ async def main() -> None:
97
+ kimi = Kimi(
98
+ base_url="https://api.moonshot.ai/v1",
99
+ api_key="your_kimi_api_key_here",
100
+ model="kimi-k2-turbo-preview",
101
+ )
102
+
103
+ history = [
104
+ Message(role="user", content="Who are you?"),
105
+ ]
106
+
107
+ def output(message_part: StreamedMessagePart):
108
+ print(message_part)
109
+
110
+ result = await kosong.generate(
111
+ chat_provider=kimi,
112
+ system_prompt="You are a helpful assistant.",
113
+ tools=[],
114
+ history=history,
115
+ on_message_part=output,
116
+ )
117
+ print(result.message)
118
+ print(result.usage)
119
+
120
+
121
+ asyncio.run(main())
122
+ ```
123
+
124
+ ### Tool calling with `kosong.step`
125
+
126
+ ```python
127
+ import asyncio
128
+
129
+ from pydantic import BaseModel
130
+
131
+ import kosong
132
+ from kosong import StepResult
133
+ from kosong.chat_provider.kimi import Kimi
134
+ from kosong.message import Message
135
+ from kosong.tooling import CallableTool2, ToolOk, ToolReturnValue
136
+ from kosong.tooling.simple import SimpleToolset
137
+
138
+
139
+ class AddToolParams(BaseModel):
140
+ a: int
141
+ b: int
142
+
143
+
144
+ class AddTool(CallableTool2[AddToolParams]):
145
+ name: str = "add"
146
+ description: str = "Add two integers."
147
+ params: type[AddToolParams] = AddToolParams
148
+
149
+ async def __call__(self, params: AddToolParams) -> ToolReturnValue:
150
+ return ToolOk(output=str(params.a + params.b))
151
+
152
+
153
+ async def main() -> None:
154
+ kimi = Kimi(
155
+ base_url="https://api.moonshot.ai/v1",
156
+ api_key="your_kimi_api_key_here",
157
+ model="kimi-k2-turbo-preview",
158
+ )
159
+
160
+ toolset = SimpleToolset()
161
+ toolset += AddTool()
162
+
163
+ history = [
164
+ Message(role="user", content="Please add 2 and 3 with the add tool."),
165
+ ]
166
+
167
+ result: StepResult = await kosong.step(
168
+ chat_provider=kimi,
169
+ system_prompt="You are a precise math tutor.",
170
+ toolset=toolset,
171
+ history=history,
172
+ )
173
+ print(result.message)
174
+ print(await result.tool_results())
175
+
176
+
177
+ asyncio.run(main())
178
+ ```
179
+
180
+ ## Builtin Demo
181
+
182
+ Kosong comes with a builtin demo agent that you can run locally. To start the demo, run:
183
+
184
+ ```sh
185
+ export KIMI_BASE_URL="https://api.moonshot.ai/v1"
186
+ export KIMI_API_KEY="your_kimi_api_key"
187
+
188
+ uv run python -m kosong kimi --with-bash
189
+ ```
190
+
191
+ ## Development
192
+
193
+ To set up a development environment, clone the repository and install the dependencies:
194
+
195
+ ```bash
196
+ git clone https://github.com/MoonshotAI/kosong.git
197
+ cd kosong
198
+ uv sync --all-extras
199
+
200
+ make check # run lint and type checks
201
+ make test # run tests
202
+ make format # format code
203
+ ```
@@ -0,0 +1,183 @@
1
+ # Kosong
2
+
3
+ Kosong is an LLM abstraction layer designed for modern AI agent applications. It unifies message structures, asynchronous tool orchestration, and pluggable chat providers so you can build agents with ease and avoid vendor lock-in.
4
+
5
+ > Kosong means "empty" in Malay and Indonesian.
6
+
7
+ ## Installation
8
+
9
+ Kosong requires Python 3.12 or higher. We recommend using uv as the package manager.
10
+
11
+ Init your project with:
12
+
13
+ ```bash
14
+ uv init --python 3.12 # or higher
15
+ ```
16
+
17
+ Then add Kosong as a dependency:
18
+
19
+ ```bash
20
+ uv add kosong
21
+ ```
22
+
23
+ To enable chat providers other than Kimi (e.g. Anthropic and Google Gemini), install the optional extra:
24
+
25
+ ```bash
26
+ uv add 'kosong[contrib]'
27
+ ```
28
+
29
+ ## Examples
30
+
31
+ ### Simple chat completion
32
+
33
+ ```python
34
+ import asyncio
35
+
36
+ import kosong
37
+ from kosong.chat_provider.kimi import Kimi
38
+ from kosong.message import Message
39
+
40
+
41
+ async def main() -> None:
42
+ kimi = Kimi(
43
+ base_url="https://api.moonshot.ai/v1",
44
+ api_key="your_kimi_api_key_here",
45
+ model="kimi-k2-turbo-preview",
46
+ )
47
+
48
+ history = [
49
+ Message(role="user", content="Who are you?"),
50
+ ]
51
+
52
+ result = await kosong.generate(
53
+ chat_provider=kimi,
54
+ system_prompt="You are a helpful assistant.",
55
+ tools=[],
56
+ history=history,
57
+ )
58
+ print(result.message)
59
+ print(result.usage)
60
+
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ ### Streaming output
66
+
67
+ ```python
68
+ import asyncio
69
+
70
+ import kosong
71
+ from kosong.chat_provider import StreamedMessagePart
72
+ from kosong.chat_provider.kimi import Kimi
73
+ from kosong.message import Message
74
+
75
+
76
+ async def main() -> None:
77
+ kimi = Kimi(
78
+ base_url="https://api.moonshot.ai/v1",
79
+ api_key="your_kimi_api_key_here",
80
+ model="kimi-k2-turbo-preview",
81
+ )
82
+
83
+ history = [
84
+ Message(role="user", content="Who are you?"),
85
+ ]
86
+
87
+ def output(message_part: StreamedMessagePart):
88
+ print(message_part)
89
+
90
+ result = await kosong.generate(
91
+ chat_provider=kimi,
92
+ system_prompt="You are a helpful assistant.",
93
+ tools=[],
94
+ history=history,
95
+ on_message_part=output,
96
+ )
97
+ print(result.message)
98
+ print(result.usage)
99
+
100
+
101
+ asyncio.run(main())
102
+ ```
103
+
104
+ ### Tool calling with `kosong.step`
105
+
106
+ ```python
107
+ import asyncio
108
+
109
+ from pydantic import BaseModel
110
+
111
+ import kosong
112
+ from kosong import StepResult
113
+ from kosong.chat_provider.kimi import Kimi
114
+ from kosong.message import Message
115
+ from kosong.tooling import CallableTool2, ToolOk, ToolReturnValue
116
+ from kosong.tooling.simple import SimpleToolset
117
+
118
+
119
+ class AddToolParams(BaseModel):
120
+ a: int
121
+ b: int
122
+
123
+
124
+ class AddTool(CallableTool2[AddToolParams]):
125
+ name: str = "add"
126
+ description: str = "Add two integers."
127
+ params: type[AddToolParams] = AddToolParams
128
+
129
+ async def __call__(self, params: AddToolParams) -> ToolReturnValue:
130
+ return ToolOk(output=str(params.a + params.b))
131
+
132
+
133
+ async def main() -> None:
134
+ kimi = Kimi(
135
+ base_url="https://api.moonshot.ai/v1",
136
+ api_key="your_kimi_api_key_here",
137
+ model="kimi-k2-turbo-preview",
138
+ )
139
+
140
+ toolset = SimpleToolset()
141
+ toolset += AddTool()
142
+
143
+ history = [
144
+ Message(role="user", content="Please add 2 and 3 with the add tool."),
145
+ ]
146
+
147
+ result: StepResult = await kosong.step(
148
+ chat_provider=kimi,
149
+ system_prompt="You are a precise math tutor.",
150
+ toolset=toolset,
151
+ history=history,
152
+ )
153
+ print(result.message)
154
+ print(await result.tool_results())
155
+
156
+
157
+ asyncio.run(main())
158
+ ```
159
+
160
+ ## Builtin Demo
161
+
162
+ Kosong comes with a builtin demo agent that you can run locally. To start the demo, run:
163
+
164
+ ```sh
165
+ export KIMI_BASE_URL="https://api.moonshot.ai/v1"
166
+ export KIMI_API_KEY="your_kimi_api_key"
167
+
168
+ uv run python -m kosong kimi --with-bash
169
+ ```
170
+
171
+ ## Development
172
+
173
+ To set up a development environment, clone the repository and install the dependencies:
174
+
175
+ ```bash
176
+ git clone https://github.com/MoonshotAI/kosong.git
177
+ cd kosong
178
+ uv sync --all-extras
179
+
180
+ make check # run lint and type checks
181
+ make test # run tests
182
+ make format # format code
183
+ ```
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "kosong-x"
3
+ version = "0.52.0"
4
+ description = "The LLM abstraction layer for modern AI agent applications."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "anthropic>=0.78.0",
9
+ "certifi>=2024.0.0",
10
+ "google-genai>=1.56.0",
11
+ "jsonschema>=4.25.1",
12
+ "loguru>=0.6.0,<0.8",
13
+ "openai>=2.14.0,<2.15.0",
14
+ "pydantic>=2.12.5",
15
+ "python-dotenv>=1.2.1",
16
+ "typing-extensions>=4.15.0",
17
+ "mcp>=1,<2",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ contrib = [
22
+ "anthropic>=0.78.0",
23
+ "google-genai>=1.55.0",
24
+ ]
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "pyright>=1.1.407",
29
+ "ty>=0.0.7",
30
+ "pytest>=9.0.2",
31
+ "pytest-asyncio>=1.3.0",
32
+ "respx>=0.22.0",
33
+ "ruff>=0.14.10",
34
+ "inline-snapshot[black]>=0.31.1",
35
+ "pdoc>=16.0.0",
36
+ ]
37
+
38
+ [build-system]
39
+ requires = ["uv_build>=0.8.5,<0.10.0"]
40
+ build-backend = "uv_build"
41
+
42
+ [tool.uv.build-backend]
43
+ module-name = ["kosong"]
44
+
45
+ [tool.ruff]
46
+ line-length = 100
47
+
48
+ [tool.ruff.format]
49
+ docstring-code-format = true
50
+
51
+ [tool.ruff.lint]
52
+ select = [
53
+ "E", # pycodestyle
54
+ "F", # Pyflakes
55
+ "UP", # pyupgrade
56
+ "B", # flake8-bugbear
57
+ "SIM", # flake8-simplify
58
+ "I", # isort
59
+ ]
60
+
61
+ [tool.pyright]
62
+ typeCheckingMode = "strict"
63
+ pythonVersion = "3.14"
64
+ include = [
65
+ "src/**/*.py",
66
+ "tests/**/*.py",
67
+ ]
68
+
69
+ [tool.ty.environment]
70
+ python-version = "3.14"
71
+
72
+ [tool.ty.src]
73
+ include = [
74
+ "src/**/*.py",
75
+ "tests/**/*.py",
76
+ ]
@@ -0,0 +1,216 @@
1
+ """
2
+ Kosong is an LLM abstraction layer designed for modern AI agent applications.
3
+ It unifies message structures, asynchronous tool orchestration, and pluggable chat providers so you
4
+ can build agents with ease and avoid vendor lock-in.
5
+
6
+ Key features:
7
+
8
+ - `kosong.generate` creates a completion stream and merges streamed message parts (including
9
+ content and tool calls) from any `ChatProvider` into a complete `Message` plus optional
10
+ `TokenUsage`.
11
+ - `kosong.step` layers tool dispatch (`Tool`, `Toolset`, `SimpleToolset`) over `generate`,
12
+ exposing `StepResult` with awaited tool outputs and streaming callbacks.
13
+ - Message structures and tool abstractions live under `kosong.message` and `kosong.tooling`.
14
+
15
+ Example:
16
+
17
+ ```python
18
+ import asyncio
19
+
20
+ from pydantic import BaseModel
21
+
22
+ import kosong
23
+ from kosong import StepResult
24
+ from kosong.chat_provider.kimi import Kimi
25
+ from kosong.message import Message
26
+ from kosong.tooling import CallableTool2, ToolOk, ToolReturnValue
27
+ from kosong.tooling.simple import SimpleToolset
28
+
29
+
30
+ class AddToolParams(BaseModel):
31
+ a: int
32
+ b: int
33
+
34
+
35
+ class AddTool(CallableTool2[AddToolParams]):
36
+ name: str = "add"
37
+ description: str = "Add two integers."
38
+ params: type[AddToolParams] = AddToolParams
39
+
40
+ async def __call__(self, params: AddToolParams) -> ToolReturnValue:
41
+ return ToolOk(output=str(params.a + params.b))
42
+
43
+
44
+ async def main() -> None:
45
+ kimi = Kimi(
46
+ base_url="https://api.moonshot.ai/v1",
47
+ api_key="your_kimi_api_key_here",
48
+ model="kimi-k2-turbo-preview",
49
+ )
50
+
51
+ toolset = SimpleToolset()
52
+ toolset += AddTool()
53
+
54
+ history = [
55
+ Message(role="user", content="Please add 2 and 3 with the add tool."),
56
+ ]
57
+
58
+ result: StepResult = await kosong.step(
59
+ chat_provider=kimi,
60
+ system_prompt="You are a precise math tutor.",
61
+ toolset=toolset,
62
+ history=history,
63
+ )
64
+ print(result.message)
65
+ print(await result.tool_results())
66
+
67
+
68
+ asyncio.run(main())
69
+ ```
70
+ """
71
+
72
+ import asyncio
73
+ from collections.abc import Callable, Sequence
74
+ from dataclasses import dataclass
75
+
76
+ from loguru import logger
77
+
78
+ from kosong._generate import GenerateResult, generate
79
+ from kosong.chat_provider import ChatProvider, ChatProviderError, StreamedMessagePart, TokenUsage
80
+ from kosong.message import Message, ToolCall
81
+ from kosong.tooling import ToolResult, ToolResultFuture, Toolset
82
+ from kosong.utils.aio import Callback
83
+
84
+ # Explicitly import submodules
85
+ from . import chat_provider, contrib, message, tooling, utils
86
+
87
+ logger.disable("kosong")
88
+
89
+ __all__ = [
90
+ # submodules
91
+ "chat_provider",
92
+ "tooling",
93
+ "message",
94
+ "utils",
95
+ "contrib",
96
+ # classes and functions
97
+ "generate",
98
+ "GenerateResult",
99
+ "step",
100
+ "StepResult",
101
+ ]
102
+
103
+
104
+ async def step(
105
+ chat_provider: ChatProvider,
106
+ system_prompt: str,
107
+ toolset: Toolset,
108
+ history: Sequence[Message],
109
+ *,
110
+ on_message_part: Callback[[StreamedMessagePart], None] | None = None,
111
+ on_tool_result: Callable[[ToolResult], None] | None = None,
112
+ ) -> "StepResult":
113
+ """
114
+ Run one agent "step". In one step, the function generates LLM response based on the given
115
+ context for exactly one time. All new message parts will be streamed to `on_message_part` in
116
+ real-time if provided. Tool calls will be handled by `toolset`. The generated message will be
117
+ returned in a `StepResult`. Depending on the toolset implementation, the tool calls may be
118
+ handled asynchronously and the results need to be fetched with `await result.tool_results()`.
119
+
120
+ The message history will NOT be modified in this function.
121
+
122
+ The token usage will be returned in the `StepResult` if available.
123
+
124
+ Raises:
125
+ APIConnectionError: If the API connection fails.
126
+ APITimeoutError: If the API request times out.
127
+ APIStatusError: If the API returns a status code of 4xx or 5xx.
128
+ APIEmptyResponseError: If the API returns an empty response.
129
+ ChatProviderError: If any other recognized chat provider error occurs.
130
+ asyncio.CancelledError: If the step is cancelled.
131
+ """
132
+
133
+ tool_calls: list[ToolCall] = []
134
+ tool_result_futures: dict[str, ToolResultFuture] = {}
135
+
136
+ def future_done_callback(future: ToolResultFuture):
137
+ if on_tool_result:
138
+ try:
139
+ result = future.result()
140
+ on_tool_result(result)
141
+ except asyncio.CancelledError:
142
+ return
143
+
144
+ async def on_tool_call(tool_call: ToolCall):
145
+ tool_calls.append(tool_call)
146
+ result = toolset.handle(tool_call)
147
+
148
+ if isinstance(result, ToolResult):
149
+ future = ToolResultFuture()
150
+ future.add_done_callback(future_done_callback)
151
+ future.set_result(result)
152
+ tool_result_futures[tool_call.id] = future
153
+ else:
154
+ result.add_done_callback(future_done_callback)
155
+ tool_result_futures[tool_call.id] = result
156
+
157
+ try:
158
+ result = await generate(
159
+ chat_provider,
160
+ system_prompt,
161
+ toolset.tools,
162
+ history,
163
+ on_message_part=on_message_part,
164
+ on_tool_call=on_tool_call,
165
+ )
166
+ except (ChatProviderError, asyncio.CancelledError):
167
+ # cancel all the futures to avoid hanging tasks
168
+ for future in tool_result_futures.values():
169
+ future.remove_done_callback(future_done_callback)
170
+ future.cancel()
171
+ await asyncio.gather(*tool_result_futures.values(), return_exceptions=True)
172
+ raise
173
+
174
+ return StepResult(
175
+ result.id,
176
+ result.message,
177
+ result.usage,
178
+ tool_calls,
179
+ tool_result_futures,
180
+ )
181
+
182
+
183
+ @dataclass(frozen=True, slots=True)
184
+ class StepResult:
185
+ id: str | None
186
+ """The ID of the generated message."""
187
+
188
+ message: Message
189
+ """The message generated in this step."""
190
+
191
+ usage: TokenUsage | None
192
+ """The token usage in this step."""
193
+
194
+ tool_calls: list[ToolCall]
195
+ """All the tool calls generated in this step."""
196
+
197
+ _tool_result_futures: dict[str, ToolResultFuture]
198
+ """@private The futures of the results of the spawned tool calls."""
199
+
200
+ async def tool_results(self) -> list[ToolResult]:
201
+ """All the tool results returned by corresponding tool calls."""
202
+ if not self._tool_result_futures:
203
+ return []
204
+
205
+ try:
206
+ results: list[ToolResult] = []
207
+ for tool_call in self.tool_calls:
208
+ future = self._tool_result_futures[tool_call.id]
209
+ result = await future
210
+ results.append(result)
211
+ return results
212
+ finally:
213
+ # one exception should cancel all the futures to avoid hanging tasks
214
+ for future in self._tool_result_futures.values():
215
+ future.cancel()
216
+ await asyncio.gather(*self._tool_result_futures.values(), return_exceptions=True)