mycode-sdk 0.4.2__tar.gz → 0.5.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.
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/PKG-INFO +26 -29
- mycode_sdk-0.5.0/README.md +86 -0
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/pyproject.toml +1 -1
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/__init__.py +2 -1
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/agent.py +125 -73
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/messages.py +9 -6
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/models_catalog.json +0 -7
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/providers/anthropic_like.py +1 -1
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/providers/base.py +2 -3
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/providers/gemini.py +1 -1
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/providers/openai_chat.py +1 -1
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/providers/openai_responses.py +1 -1
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/session.py +151 -233
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/tools.py +394 -483
- mycode_sdk-0.4.2/README.md +0 -89
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/.gitignore +0 -0
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/LICENSE +0 -0
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/models.py +0 -0
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/py.typed +0 -0
- {mycode_sdk-0.4.2 → mycode_sdk-0.5.0}/src/mycode/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycode-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Multi-turn tool-calling agent runtime for embedding the mycode agent loop.
|
|
5
5
|
Project-URL: Homepage, https://github.com/legibet/mycode
|
|
6
6
|
Project-URL: Repository, https://github.com/legibet/mycode
|
|
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
# mycode-sdk
|
|
27
27
|
|
|
28
|
-
Lightweight Python SDK for
|
|
28
|
+
Lightweight Python SDK for building agents.
|
|
29
29
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
@@ -35,9 +35,7 @@ uv add mycode-sdk
|
|
|
35
35
|
pip install mycode-sdk
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
## Quick
|
|
39
|
-
|
|
40
|
-
`Agent(...)` fills in sensible defaults: provider inferred from the model id, `session_id` generated, session log auto-persisted to `~/.mycode/sessions/<session_id>/`. By default no tools are registered — pick the built-ins you want or register your own via `@tool`.
|
|
38
|
+
## Quick start
|
|
41
39
|
|
|
42
40
|
```python
|
|
43
41
|
import asyncio
|
|
@@ -50,31 +48,43 @@ async def main() -> None:
|
|
|
50
48
|
model="claude-sonnet-4-6",
|
|
51
49
|
api_key="YOUR_API_KEY",
|
|
52
50
|
cwd=".",
|
|
53
|
-
system="You are a concise coding assistant.",
|
|
54
51
|
tools=[read_tool, bash_tool],
|
|
55
52
|
)
|
|
56
53
|
|
|
57
54
|
async for event in agent.achat("Read pyproject.toml and tell me the project name."):
|
|
58
55
|
if event.type == "text":
|
|
59
|
-
print(event.data["delta"], end="")
|
|
56
|
+
print(event.data["delta"], end="", flush=True)
|
|
60
57
|
|
|
61
58
|
|
|
62
59
|
asyncio.run(main())
|
|
63
60
|
```
|
|
64
61
|
|
|
65
|
-
|
|
62
|
+
`Agent(...)` infers the provider from the model id. No tools are registered unless you pass `tools=[...]`, and nothing is persisted unless you pass `session_dir=`.
|
|
66
63
|
|
|
67
|
-
|
|
64
|
+
For a synchronous call, use `run()` — it collects the stream into a `RunResult`:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
result = agent.run("Read pyproject.toml and tell me the project name.")
|
|
68
|
+
print(result.text)
|
|
69
|
+
```
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
Call `achat` or `run` again on the same `Agent` to continue the conversation — history accumulates in `agent.messages`.
|
|
72
|
+
|
|
73
|
+
To persist across processes, pass `session_dir` as the root directory; each `session_id` becomes a subdirectory. Reconstruct with the same `(session_dir, session_id)` to resume.
|
|
74
|
+
|
|
75
|
+
## Built-in tools
|
|
70
76
|
|
|
71
77
|
```python
|
|
72
78
|
from mycode import read_tool, write_tool, edit_tool, bash_tool
|
|
73
79
|
```
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
Only `bash_tool` streams incremental output as `tool_output` events; the others return a single result.
|
|
82
|
+
|
|
83
|
+
## Custom tools
|
|
84
|
+
|
|
85
|
+
`@tool` wraps a sync or `async def` Python function as a `ToolSpec`. Parameter type hints become the JSON schema sent to the provider.
|
|
76
86
|
|
|
77
|
-
|
|
87
|
+
Annotate the first parameter as `ToolContext` to have the context injected. Use `ctx.read / ctx.write / ctx.edit / ctx.bash` to invoke the built-ins, or `ctx.call(name, args)` for any registered tool by name.
|
|
78
88
|
|
|
79
89
|
```python
|
|
80
90
|
from mycode import Agent, ToolContext, read_tool, tool
|
|
@@ -84,8 +94,8 @@ from mycode import Agent, ToolContext, read_tool, tool
|
|
|
84
94
|
def summarize_file(ctx: ToolContext, path: str) -> str:
|
|
85
95
|
"""Return the first line of a text file."""
|
|
86
96
|
|
|
87
|
-
result = ctx.
|
|
88
|
-
return result.
|
|
97
|
+
result = ctx.read(path)
|
|
98
|
+
return result.output.splitlines()[0] if result.output else ""
|
|
89
99
|
|
|
90
100
|
|
|
91
101
|
agent = Agent(
|
|
@@ -96,19 +106,6 @@ agent = Agent(
|
|
|
96
106
|
)
|
|
97
107
|
```
|
|
98
108
|
|
|
99
|
-
|
|
109
|
+
A bare `str` return becomes the tool `output`; any other JSON-serializable value is dumped to JSON. For finer control, return a `ToolExecutionResult` to set `output`, `content` (multimodal blocks such as images), `metadata` (structured UI data), and `is_error` independently.
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Point `session_dir` (or the implied `SessionStore` data dir) at a temporary directory if you need an ephemeral session:
|
|
104
|
-
|
|
105
|
-
```python
|
|
106
|
-
import tempfile
|
|
107
|
-
from pathlib import Path
|
|
108
|
-
|
|
109
|
-
agent = Agent(
|
|
110
|
-
model="claude-sonnet-4-6",
|
|
111
|
-
cwd=".",
|
|
112
|
-
session_dir=Path(tempfile.mkdtemp()) / "scratch",
|
|
113
|
-
)
|
|
114
|
-
```
|
|
111
|
+
See [docs/sdk.md](../docs/sdk.md) for the event stream, cancellation, session rules, and the full `Agent` / `@tool` reference.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# mycode-sdk
|
|
2
|
+
|
|
3
|
+
Lightweight Python SDK for building agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add mycode-sdk
|
|
9
|
+
# or
|
|
10
|
+
pip install mycode-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
from mycode import Agent, bash_tool, read_tool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def main() -> None:
|
|
22
|
+
agent = Agent(
|
|
23
|
+
model="claude-sonnet-4-6",
|
|
24
|
+
api_key="YOUR_API_KEY",
|
|
25
|
+
cwd=".",
|
|
26
|
+
tools=[read_tool, bash_tool],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
async for event in agent.achat("Read pyproject.toml and tell me the project name."):
|
|
30
|
+
if event.type == "text":
|
|
31
|
+
print(event.data["delta"], end="", flush=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
asyncio.run(main())
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`Agent(...)` infers the provider from the model id. No tools are registered unless you pass `tools=[...]`, and nothing is persisted unless you pass `session_dir=`.
|
|
38
|
+
|
|
39
|
+
For a synchronous call, use `run()` — it collects the stream into a `RunResult`:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
result = agent.run("Read pyproject.toml and tell me the project name.")
|
|
43
|
+
print(result.text)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Call `achat` or `run` again on the same `Agent` to continue the conversation — history accumulates in `agent.messages`.
|
|
47
|
+
|
|
48
|
+
To persist across processes, pass `session_dir` as the root directory; each `session_id` becomes a subdirectory. Reconstruct with the same `(session_dir, session_id)` to resume.
|
|
49
|
+
|
|
50
|
+
## Built-in tools
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from mycode import read_tool, write_tool, edit_tool, bash_tool
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Only `bash_tool` streams incremental output as `tool_output` events; the others return a single result.
|
|
57
|
+
|
|
58
|
+
## Custom tools
|
|
59
|
+
|
|
60
|
+
`@tool` wraps a sync or `async def` Python function as a `ToolSpec`. Parameter type hints become the JSON schema sent to the provider.
|
|
61
|
+
|
|
62
|
+
Annotate the first parameter as `ToolContext` to have the context injected. Use `ctx.read / ctx.write / ctx.edit / ctx.bash` to invoke the built-ins, or `ctx.call(name, args)` for any registered tool by name.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from mycode import Agent, ToolContext, read_tool, tool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@tool
|
|
69
|
+
def summarize_file(ctx: ToolContext, path: str) -> str:
|
|
70
|
+
"""Return the first line of a text file."""
|
|
71
|
+
|
|
72
|
+
result = ctx.read(path)
|
|
73
|
+
return result.output.splitlines()[0] if result.output else ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
agent = Agent(
|
|
77
|
+
model="claude-sonnet-4-6",
|
|
78
|
+
api_key="YOUR_API_KEY",
|
|
79
|
+
cwd=".",
|
|
80
|
+
tools=[read_tool, summarize_file],
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A bare `str` return becomes the tool `output`; any other JSON-serializable value is dumped to JSON. For finer control, return a `ToolExecutionResult` to set `output`, `content` (multimodal blocks such as images), `metadata` (structured UI data), and `is_error` independently.
|
|
85
|
+
|
|
86
|
+
See [docs/sdk.md](../docs/sdk.md) for the event stream, cancellation, session rules, and the full `Agent` / `@tool` reference.
|
|
@@ -7,7 +7,7 @@ runtime ships four built-in coding tools (``read``, ``write``, ``edit``,
|
|
|
7
7
|
silently exposing file system and shell access.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from mycode.agent import Agent, Event, PersistCallback
|
|
10
|
+
from mycode.agent import Agent, Event, PersistCallback, RunResult
|
|
11
11
|
from mycode.messages import (
|
|
12
12
|
ContentBlock,
|
|
13
13
|
ConversationMessage,
|
|
@@ -47,6 +47,7 @@ __all__ = [
|
|
|
47
47
|
"DEFAULT_TOOL_SPECS",
|
|
48
48
|
"Event",
|
|
49
49
|
"PersistCallback",
|
|
50
|
+
"RunResult",
|
|
50
51
|
"SessionStore",
|
|
51
52
|
"ToolContext",
|
|
52
53
|
"ToolExecutionResult",
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"""Multi-turn agent loop.
|
|
2
2
|
|
|
3
3
|
:class:`Agent` drives one conversation. Each call to :meth:`Agent.achat`
|
|
4
|
-
runs one user turn
|
|
5
|
-
session log.
|
|
4
|
+
runs one user turn; when a ``session_dir`` is configured, every emitted
|
|
5
|
+
message is appended to the on-disk session log.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import logging
|
|
12
|
+
import tempfile
|
|
12
13
|
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
|
13
14
|
from dataclasses import dataclass, field
|
|
14
15
|
from pathlib import Path
|
|
@@ -31,10 +32,9 @@ from mycode.session import (
|
|
|
31
32
|
SessionStore,
|
|
32
33
|
apply_compact,
|
|
33
34
|
build_compact_event,
|
|
34
|
-
resolve_sessions_dir,
|
|
35
35
|
should_compact,
|
|
36
36
|
)
|
|
37
|
-
from mycode.tools import ToolExecutionResult, ToolExecutor, ToolSpec
|
|
37
|
+
from mycode.tools import ToolContext, ToolExecutionResult, ToolExecutor, ToolSpec
|
|
38
38
|
|
|
39
39
|
logger = logging.getLogger(__name__)
|
|
40
40
|
|
|
@@ -49,6 +49,15 @@ class Event:
|
|
|
49
49
|
data: dict[str, Any] = field(default_factory=dict)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
@dataclass
|
|
53
|
+
class RunResult:
|
|
54
|
+
"""Collected result returned by :meth:`Agent.run`."""
|
|
55
|
+
|
|
56
|
+
text: str = ""
|
|
57
|
+
events: list[Event] = field(default_factory=list)
|
|
58
|
+
error: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
52
61
|
class Agent:
|
|
53
62
|
"""Multi-turn tool-calling agent runtime."""
|
|
54
63
|
|
|
@@ -58,8 +67,8 @@ class Agent:
|
|
|
58
67
|
model: str,
|
|
59
68
|
cwd: str,
|
|
60
69
|
provider: str | None = None,
|
|
61
|
-
session_id: str | None = None,
|
|
62
70
|
session_dir: Path | None = None,
|
|
71
|
+
session_id: str | None = None,
|
|
63
72
|
api_key: str | None = None,
|
|
64
73
|
api_base: str | None = None,
|
|
65
74
|
messages: list[ConversationMessage] | None = None,
|
|
@@ -72,7 +81,7 @@ class Agent:
|
|
|
72
81
|
supports_image_input: bool | None = None,
|
|
73
82
|
supports_pdf_input: bool | None = None,
|
|
74
83
|
system: str = "",
|
|
75
|
-
tools:
|
|
84
|
+
tools: Sequence[ToolSpec] = (),
|
|
76
85
|
):
|
|
77
86
|
self.model = model
|
|
78
87
|
if provider is None:
|
|
@@ -83,10 +92,13 @@ class Agent:
|
|
|
83
92
|
self.provider = provider
|
|
84
93
|
|
|
85
94
|
self.cwd = str(Path(cwd).resolve(strict=False))
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
|
|
96
|
+
# Persistence is opt-in: a store is only created when ``session_dir``
|
|
97
|
+
# is supplied. ``session_id`` is always populated (uuid when absent)
|
|
98
|
+
# so Events can carry a stable runtime tag even in memory-only mode.
|
|
99
|
+
self.session_dir = session_dir
|
|
100
|
+
self.session_id = (session_id or "").strip() or uuid4().hex
|
|
101
|
+
self._store: SessionStore | None = SessionStore(data_dir=session_dir) if session_dir is not None else None
|
|
90
102
|
|
|
91
103
|
self.api_key = api_key
|
|
92
104
|
self.api_base = api_base
|
|
@@ -98,20 +110,38 @@ class Agent:
|
|
|
98
110
|
self._cancel_event = asyncio.Event()
|
|
99
111
|
self._provider_event_task: asyncio.Future[ProviderStreamEvent] | None = None
|
|
100
112
|
|
|
113
|
+
# History resolution:
|
|
114
|
+
# - messages is None → auto-resume from disk if the session exists
|
|
115
|
+
# - messages is [] or [...] → use as-is; refuse if it would overwrite disk
|
|
101
116
|
if messages is None:
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
if self._store is not None:
|
|
118
|
+
data = self._store.load_session_sync(self.session_id)
|
|
119
|
+
messages = list(data["messages"]) if data is not None else []
|
|
120
|
+
else:
|
|
121
|
+
messages = []
|
|
122
|
+
elif self._store is not None and self._store.session_exists(self.session_id):
|
|
123
|
+
msg = (
|
|
124
|
+
f"session {self.session_id!r} already exists on disk; "
|
|
125
|
+
"pass messages=None to resume or choose a different session_id"
|
|
126
|
+
)
|
|
127
|
+
raise ValueError(msg)
|
|
104
128
|
self.messages: list[ConversationMessage] = list(messages)
|
|
105
129
|
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
# Tool runtime: one executor per agent. ``tool_output_dir`` is where
|
|
131
|
+
# tools can drop artifacts — defaults to a session-adjacent directory
|
|
132
|
+
# so logs (e.g. bash spill files) live next to the session JSONL.
|
|
133
|
+
# When no session is configured, use a tempdir scoped to session_id.
|
|
134
|
+
if session_dir is not None:
|
|
135
|
+
tool_output_dir = session_dir / self.session_id / "tool-output"
|
|
108
136
|
else:
|
|
109
|
-
self.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
tool_output_dir = Path(tempfile.gettempdir()) / "mycode" / self.session_id / "tool-output"
|
|
138
|
+
self.tools = ToolExecutor(tools)
|
|
139
|
+
self.tool_ctx = ToolContext(
|
|
140
|
+
executor=self.tools,
|
|
141
|
+
cwd=self.cwd,
|
|
142
|
+
tool_output_dir=tool_output_dir,
|
|
143
|
+
supports_image_input=False,
|
|
144
|
+
)
|
|
115
145
|
|
|
116
146
|
self.refresh_capabilities(
|
|
117
147
|
max_tokens=max_tokens,
|
|
@@ -151,7 +181,7 @@ class Agent:
|
|
|
151
181
|
self.supports_reasoning: bool | None = meta.supports_reasoning
|
|
152
182
|
self.supports_image_input: bool = bool(meta.supports_image_input)
|
|
153
183
|
self.supports_pdf_input: bool = bool(meta.supports_pdf_input)
|
|
154
|
-
self.
|
|
184
|
+
self.tool_ctx.supports_image_input = self.supports_image_input
|
|
155
185
|
|
|
156
186
|
def cancel(self) -> None:
|
|
157
187
|
"""Request cancellation of the in-flight turn."""
|
|
@@ -183,11 +213,7 @@ class Agent:
|
|
|
183
213
|
if self._cancel_event.is_set():
|
|
184
214
|
yield self._tool_done_event(
|
|
185
215
|
tool_id,
|
|
186
|
-
ToolExecutionResult(
|
|
187
|
-
model_text="error: cancelled",
|
|
188
|
-
display_text="Cancelled",
|
|
189
|
-
is_error=True,
|
|
190
|
-
),
|
|
216
|
+
ToolExecutionResult(output="error: cancelled", is_error=True),
|
|
191
217
|
)
|
|
192
218
|
return
|
|
193
219
|
|
|
@@ -195,11 +221,7 @@ class Agent:
|
|
|
195
221
|
if spec is None:
|
|
196
222
|
yield self._tool_done_event(
|
|
197
223
|
tool_id,
|
|
198
|
-
ToolExecutionResult(
|
|
199
|
-
model_text=f"error: unknown tool: {name}",
|
|
200
|
-
display_text=f"Unknown tool: {name}",
|
|
201
|
-
is_error=True,
|
|
202
|
-
),
|
|
224
|
+
ToolExecutionResult(output=f"error: unknown tool: {name}", is_error=True),
|
|
203
225
|
)
|
|
204
226
|
return
|
|
205
227
|
|
|
@@ -209,13 +231,10 @@ class Agent:
|
|
|
209
231
|
return
|
|
210
232
|
|
|
211
233
|
try:
|
|
212
|
-
|
|
234
|
+
ctx = self._ctx_for_call(tool_id)
|
|
235
|
+
result = await asyncio.to_thread(self.tools.execute, name, args, ctx)
|
|
213
236
|
except Exception as exc: # pragma: no cover - defensive
|
|
214
|
-
result = ToolExecutionResult(
|
|
215
|
-
model_text=f"error: {exc}",
|
|
216
|
-
display_text=str(exc),
|
|
217
|
-
is_error=True,
|
|
218
|
-
)
|
|
237
|
+
result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
|
|
219
238
|
|
|
220
239
|
yield self._tool_done_event(tool_id, result)
|
|
221
240
|
|
|
@@ -234,15 +253,11 @@ class Agent:
|
|
|
234
253
|
def on_output(line: str) -> None:
|
|
235
254
|
loop.call_soon_threadsafe(output_queue.put_nowait, line)
|
|
236
255
|
|
|
256
|
+
ctx = self._ctx_for_call(tool_id, emit=on_output)
|
|
257
|
+
|
|
237
258
|
async def run_in_thread() -> ToolExecutionResult:
|
|
238
259
|
try:
|
|
239
|
-
return await asyncio.to_thread(
|
|
240
|
-
self.tools.execute,
|
|
241
|
-
name,
|
|
242
|
-
args,
|
|
243
|
-
tool_call_id=tool_id,
|
|
244
|
-
on_output=on_output,
|
|
245
|
-
)
|
|
260
|
+
return await asyncio.to_thread(self.tools.execute, name, args, ctx)
|
|
246
261
|
finally:
|
|
247
262
|
loop.call_soon_threadsafe(output_queue.put_nowait, None)
|
|
248
263
|
|
|
@@ -271,31 +286,41 @@ class Agent:
|
|
|
271
286
|
await task
|
|
272
287
|
except Exception:
|
|
273
288
|
pass
|
|
274
|
-
result = ToolExecutionResult(
|
|
275
|
-
model_text="error: cancelled",
|
|
276
|
-
display_text="Cancelled",
|
|
277
|
-
is_error=True,
|
|
278
|
-
)
|
|
289
|
+
result = ToolExecutionResult(output="error: cancelled", is_error=True)
|
|
279
290
|
else:
|
|
280
291
|
try:
|
|
281
292
|
result = await task
|
|
282
293
|
except Exception as exc: # pragma: no cover - defensive
|
|
283
|
-
result = ToolExecutionResult(
|
|
284
|
-
model_text=f"error: {exc}",
|
|
285
|
-
display_text=str(exc),
|
|
286
|
-
is_error=True,
|
|
287
|
-
)
|
|
294
|
+
result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
|
|
288
295
|
|
|
289
296
|
yield self._tool_done_event(tool_id, result)
|
|
290
297
|
|
|
298
|
+
def _ctx_for_call(
|
|
299
|
+
self,
|
|
300
|
+
tool_id: str,
|
|
301
|
+
*,
|
|
302
|
+
emit: Callable[[str], None] | None = None,
|
|
303
|
+
) -> ToolContext:
|
|
304
|
+
"""Build a per-call ToolContext from the base context."""
|
|
305
|
+
|
|
306
|
+
return ToolContext(
|
|
307
|
+
executor=self.tools,
|
|
308
|
+
cwd=self.tool_ctx.cwd,
|
|
309
|
+
tool_output_dir=self.tool_ctx.tool_output_dir,
|
|
310
|
+
supports_image_input=self.tool_ctx.supports_image_input,
|
|
311
|
+
tool_call_id=tool_id,
|
|
312
|
+
emit=emit,
|
|
313
|
+
)
|
|
314
|
+
|
|
291
315
|
@staticmethod
|
|
292
316
|
def _tool_done_event(tool_id: str, result: ToolExecutionResult) -> Event:
|
|
293
317
|
data: dict[str, Any] = {
|
|
294
318
|
"tool_use_id": tool_id,
|
|
295
|
-
"
|
|
296
|
-
"display_text": result.display_text,
|
|
319
|
+
"output": result.output,
|
|
297
320
|
"is_error": result.is_error,
|
|
298
321
|
}
|
|
322
|
+
if result.metadata:
|
|
323
|
+
data["metadata"] = result.metadata
|
|
299
324
|
if result.content:
|
|
300
325
|
data["content"] = result.content
|
|
301
326
|
return Event("tool_done", data)
|
|
@@ -337,7 +362,7 @@ class Agent:
|
|
|
337
362
|
pass
|
|
338
363
|
|
|
339
364
|
# ------------------------------------------------------------------
|
|
340
|
-
# Public entry
|
|
365
|
+
# Public entry points
|
|
341
366
|
# ------------------------------------------------------------------
|
|
342
367
|
|
|
343
368
|
async def achat(
|
|
@@ -352,27 +377,26 @@ class Agent:
|
|
|
352
377
|
requests tools, the agent runs them locally, appends one user-side
|
|
353
378
|
tool_result message, and continues until the assistant stops using tools.
|
|
354
379
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
380
|
+
When a ``session_dir`` is configured, every emitted message is appended
|
|
381
|
+
to the on-disk session log. ``on_persist`` fires *before* that append
|
|
382
|
+
regardless of whether a store is configured, so callers can plug in a
|
|
383
|
+
custom backend or stage related records (the web server uses it to
|
|
384
|
+
land a rewind marker first).
|
|
358
385
|
"""
|
|
359
386
|
|
|
360
387
|
async def persist(message: ConversationMessage) -> None:
|
|
361
388
|
if on_persist is not None:
|
|
362
389
|
await on_persist(message)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
cwd=self.cwd,
|
|
369
|
-
api_base=self.api_base,
|
|
370
|
-
)
|
|
390
|
+
if self._store is None:
|
|
391
|
+
return
|
|
392
|
+
if not self._store.session_exists(self.session_id):
|
|
393
|
+
await self._store.create_session(self.session_id, cwd=self.cwd)
|
|
394
|
+
await self._store.append_message(self.session_id, message)
|
|
371
395
|
|
|
372
396
|
self._cancel_event.clear()
|
|
373
397
|
supports_image_input = self.supports_image_input
|
|
374
398
|
supports_pdf_input = self.supports_pdf_input
|
|
375
|
-
self.
|
|
399
|
+
self.tool_ctx.supports_image_input = supports_image_input
|
|
376
400
|
|
|
377
401
|
if isinstance(user_input, str):
|
|
378
402
|
user_message = user_text_message(user_input)
|
|
@@ -491,19 +515,20 @@ class Agent:
|
|
|
491
515
|
continue
|
|
492
516
|
|
|
493
517
|
d = event.data
|
|
494
|
-
|
|
518
|
+
output = str(d.get("output") or "")
|
|
519
|
+
metadata = d.get("metadata") if isinstance(d.get("metadata"), dict) else None
|
|
495
520
|
content = d.get("content")
|
|
496
521
|
tool_results.append(
|
|
497
522
|
tool_result_block(
|
|
498
523
|
tool_use_id=str(d.get("tool_use_id") or ""),
|
|
499
|
-
|
|
500
|
-
|
|
524
|
+
output=output,
|
|
525
|
+
metadata=metadata,
|
|
501
526
|
is_error=bool(d.get("is_error")),
|
|
502
527
|
content=content if isinstance(content, list) else None,
|
|
503
528
|
)
|
|
504
529
|
)
|
|
505
530
|
|
|
506
|
-
if
|
|
531
|
+
if output == "error: cancelled" and self._cancel_event.is_set():
|
|
507
532
|
tool_result_message = build_message("user", tool_results)
|
|
508
533
|
self.messages.append(tool_result_message)
|
|
509
534
|
await persist(tool_result_message)
|
|
@@ -524,6 +549,33 @@ class Agent:
|
|
|
524
549
|
async for event in self._compact_if_needed(adapter, persist):
|
|
525
550
|
yield event
|
|
526
551
|
|
|
552
|
+
def run(
|
|
553
|
+
self,
|
|
554
|
+
user_input: str | ConversationMessage,
|
|
555
|
+
*,
|
|
556
|
+
on_persist: PersistCallback | None = None,
|
|
557
|
+
) -> RunResult:
|
|
558
|
+
"""Run one user turn synchronously and collect the streamed result."""
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
asyncio.get_running_loop()
|
|
562
|
+
except RuntimeError:
|
|
563
|
+
pass
|
|
564
|
+
else:
|
|
565
|
+
raise RuntimeError("Agent.run() cannot run inside an active event loop; use Agent.achat() instead")
|
|
566
|
+
|
|
567
|
+
async def collect() -> RunResult:
|
|
568
|
+
result = RunResult()
|
|
569
|
+
async for event in self.achat(user_input, on_persist=on_persist):
|
|
570
|
+
result.events.append(event)
|
|
571
|
+
if event.type == "text":
|
|
572
|
+
result.text += str(event.data.get("delta") or "")
|
|
573
|
+
elif event.type == "error" and result.error is None:
|
|
574
|
+
result.error = str(event.data.get("message") or "")
|
|
575
|
+
return result
|
|
576
|
+
|
|
577
|
+
return asyncio.run(collect())
|
|
578
|
+
|
|
527
579
|
# ------------------------------------------------------------------
|
|
528
580
|
# Context compaction
|
|
529
581
|
# ------------------------------------------------------------------
|
|
@@ -92,25 +92,28 @@ def tool_use_block(
|
|
|
92
92
|
def tool_result_block(
|
|
93
93
|
*,
|
|
94
94
|
tool_use_id: str,
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
output: str,
|
|
96
|
+
metadata: dict[str, Any] | None = None,
|
|
97
97
|
is_error: bool = False,
|
|
98
98
|
content: list[ContentBlock] | None = None,
|
|
99
99
|
meta: dict[str, Any] | None = None,
|
|
100
100
|
) -> ContentBlock:
|
|
101
101
|
"""Build a tool-result block.
|
|
102
102
|
|
|
103
|
-
`
|
|
104
|
-
`
|
|
103
|
+
`output` is replayed back to providers on later turns.
|
|
104
|
+
`content` carries multimodal blocks (e.g. images) that providers should
|
|
105
|
+
replay alongside the text. `metadata` is an optional structured payload
|
|
106
|
+
for UI consumption (e.g. edit diff line numbers).
|
|
105
107
|
"""
|
|
106
108
|
|
|
107
109
|
block: ContentBlock = {
|
|
108
110
|
"type": "tool_result",
|
|
109
111
|
"tool_use_id": tool_use_id,
|
|
110
|
-
"
|
|
111
|
-
"display_text": display_text,
|
|
112
|
+
"output": output,
|
|
112
113
|
"is_error": is_error,
|
|
113
114
|
}
|
|
115
|
+
if metadata:
|
|
116
|
+
block["metadata"] = dict(metadata)
|
|
114
117
|
if content:
|
|
115
118
|
block["content"] = [dict(item) for item in content]
|
|
116
119
|
if meta:
|
|
@@ -901,13 +901,6 @@
|
|
|
901
901
|
"supports_pdf_input": false,
|
|
902
902
|
"supports_reasoning": false
|
|
903
903
|
},
|
|
904
|
-
"codex-mini-latest": {
|
|
905
|
-
"context_window": 200000,
|
|
906
|
-
"max_output_tokens": 100000,
|
|
907
|
-
"supports_image_input": false,
|
|
908
|
-
"supports_pdf_input": false,
|
|
909
|
-
"supports_reasoning": true
|
|
910
|
-
},
|
|
911
904
|
"gpt-3.5-turbo": {
|
|
912
905
|
"context_window": 16385,
|
|
913
906
|
"max_output_tokens": 4096,
|
|
@@ -300,7 +300,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
300
300
|
return {
|
|
301
301
|
"type": "tool_result",
|
|
302
302
|
"tool_use_id": block.get("tool_use_id"),
|
|
303
|
-
"content": content_blocks or str(block.get("
|
|
303
|
+
"content": content_blocks or str(block.get("output") or ""),
|
|
304
304
|
"is_error": bool(block.get("is_error")),
|
|
305
305
|
}
|
|
306
306
|
|
|
@@ -338,8 +338,7 @@ def _interrupted_tool_result_message(tool_use_ids: list[str]) -> ConversationMes
|
|
|
338
338
|
[
|
|
339
339
|
tool_result_block(
|
|
340
340
|
tool_use_id=tool_use_id,
|
|
341
|
-
|
|
342
|
-
display_text="Tool call was interrupted",
|
|
341
|
+
output="error: tool call was interrupted",
|
|
343
342
|
is_error=True,
|
|
344
343
|
)
|
|
345
344
|
for tool_use_id in tool_use_ids
|
|
@@ -384,4 +383,4 @@ def tool_result_content_blocks(block: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
384
383
|
structured = [dict(item) for item in raw_content if isinstance(item, dict)]
|
|
385
384
|
if structured:
|
|
386
385
|
return structured
|
|
387
|
-
return [text_block(str(block.get("
|
|
386
|
+
return [text_block(str(block.get("output") or ""))]
|
|
@@ -198,7 +198,7 @@ class GoogleGeminiAdapter(ProviderAdapter):
|
|
|
198
198
|
continue
|
|
199
199
|
|
|
200
200
|
tool_id = str(block.get("tool_use_id") or "")
|
|
201
|
-
response: dict[str, Any] = {"result": str(block.get("
|
|
201
|
+
response: dict[str, Any] = {"result": str(block.get("output") or "")}
|
|
202
202
|
if block.get("is_error"):
|
|
203
203
|
response["is_error"] = True
|
|
204
204
|
|