mycode-sdk 0.8.10__tar.gz → 0.9.1__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.8.10 → mycode_sdk-0.9.1}/PKG-INFO +35 -7
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/README.md +29 -3
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/pyproject.toml +6 -4
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/__init__.py +7 -13
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/agent.py +99 -88
- mycode_sdk-0.9.1/src/mycode/attachments.py +176 -0
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/models_catalog.json +146 -328
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/providers/anthropic_like.py +14 -12
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/providers/base.py +27 -17
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/providers/gemini.py +3 -3
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/providers/openai_chat.py +52 -47
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/providers/openai_responses.py +133 -101
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/session.py +55 -18
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/tools.py +372 -455
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/utils.py +10 -0
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/.gitignore +0 -0
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/LICENSE +0 -0
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/compact.py +0 -0
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/hooks.py +0 -0
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/messages.py +0 -0
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/models.py +0 -0
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.8.10 → mycode_sdk-0.9.1}/src/mycode/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycode-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Lightweight Python SDK for building AI agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/legibet/mycode
|
|
6
6
|
Project-URL: Repository, https://github.com/legibet/mycode
|
|
@@ -18,9 +18,11 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Topic :: Software Development
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
|
-
Requires-Dist: anthropic>=0.
|
|
22
|
-
Requires-Dist: google-genai>=2.
|
|
23
|
-
Requires-Dist:
|
|
21
|
+
Requires-Dist: anthropic>=0.106.0
|
|
22
|
+
Requires-Dist: google-genai>=2.8.0
|
|
23
|
+
Requires-Dist: griffelib>=2.0.0
|
|
24
|
+
Requires-Dist: openai>=2.41.0
|
|
25
|
+
Requires-Dist: pydantic>=2.13.0
|
|
24
26
|
Description-Content-Type: text/markdown
|
|
25
27
|
|
|
26
28
|
# mycode-sdk
|
|
@@ -78,6 +80,28 @@ agent.run("What is 2 + 2?")
|
|
|
78
80
|
agent.run("Now multiply that by 10.") # remembers the earlier answer
|
|
79
81
|
```
|
|
80
82
|
|
|
83
|
+
## Attachments
|
|
84
|
+
|
|
85
|
+
Pass `attachments` to `achat()` or `run()` to add files alongside the prompt:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from mycode import Attachment
|
|
89
|
+
|
|
90
|
+
agent.run("Describe these.", attachments=["diagram.png", "report.pdf", "notes.txt"])
|
|
91
|
+
|
|
92
|
+
# Or build them explicitly — raw bytes and inline text never touch the disk:
|
|
93
|
+
agent.run(
|
|
94
|
+
"Review.",
|
|
95
|
+
attachments=[
|
|
96
|
+
Attachment.path("diagram.png"),
|
|
97
|
+
Attachment.bytes(png_data, media_type="image/png"),
|
|
98
|
+
Attachment.text("TODO: ship it", name="note.md"),
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Images support `image/png`, `image/jpeg`, `image/gif`, `image/webp`; documents support `application/pdf`. Sending an image or PDF to a model that lacks that capability yields an `error` event; a bad path or unsupported type raises `ValueError` before the model is called.
|
|
104
|
+
|
|
81
105
|
## Saving sessions
|
|
82
106
|
|
|
83
107
|
Pass `session_dir` to persist the conversation to disk. Each session lives in a subdirectory named by `session_id`:
|
|
@@ -105,7 +129,7 @@ Four tools for reading, writing, editing files, and running shell commands. Opt
|
|
|
105
129
|
|
|
106
130
|
## Custom tools
|
|
107
131
|
|
|
108
|
-
Decorate
|
|
132
|
+
Decorate a typed function with `@tool`:
|
|
109
133
|
|
|
110
134
|
```python
|
|
111
135
|
from mycode import Agent, tool
|
|
@@ -113,7 +137,11 @@ from mycode import Agent, tool
|
|
|
113
137
|
|
|
114
138
|
@tool
|
|
115
139
|
def greet(name: str) -> str:
|
|
116
|
-
"""Return a friendly greeting.
|
|
140
|
+
"""Return a friendly greeting.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
name: Person name.
|
|
144
|
+
"""
|
|
117
145
|
|
|
118
146
|
return f"hello, {name}"
|
|
119
147
|
|
|
@@ -125,7 +153,7 @@ agent = Agent(
|
|
|
125
153
|
)
|
|
126
154
|
```
|
|
127
155
|
|
|
128
|
-
To call a built-in tool from inside your own, type the first parameter as `ToolContext`:
|
|
156
|
+
To call a built-in tool from inside your own tool, type the first parameter as `ToolContext`:
|
|
129
157
|
|
|
130
158
|
```python
|
|
131
159
|
from mycode import ToolContext, tool
|
|
@@ -53,6 +53,28 @@ agent.run("What is 2 + 2?")
|
|
|
53
53
|
agent.run("Now multiply that by 10.") # remembers the earlier answer
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## Attachments
|
|
57
|
+
|
|
58
|
+
Pass `attachments` to `achat()` or `run()` to add files alongside the prompt:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from mycode import Attachment
|
|
62
|
+
|
|
63
|
+
agent.run("Describe these.", attachments=["diagram.png", "report.pdf", "notes.txt"])
|
|
64
|
+
|
|
65
|
+
# Or build them explicitly — raw bytes and inline text never touch the disk:
|
|
66
|
+
agent.run(
|
|
67
|
+
"Review.",
|
|
68
|
+
attachments=[
|
|
69
|
+
Attachment.path("diagram.png"),
|
|
70
|
+
Attachment.bytes(png_data, media_type="image/png"),
|
|
71
|
+
Attachment.text("TODO: ship it", name="note.md"),
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Images support `image/png`, `image/jpeg`, `image/gif`, `image/webp`; documents support `application/pdf`. Sending an image or PDF to a model that lacks that capability yields an `error` event; a bad path or unsupported type raises `ValueError` before the model is called.
|
|
77
|
+
|
|
56
78
|
## Saving sessions
|
|
57
79
|
|
|
58
80
|
Pass `session_dir` to persist the conversation to disk. Each session lives in a subdirectory named by `session_id`:
|
|
@@ -80,7 +102,7 @@ Four tools for reading, writing, editing files, and running shell commands. Opt
|
|
|
80
102
|
|
|
81
103
|
## Custom tools
|
|
82
104
|
|
|
83
|
-
Decorate
|
|
105
|
+
Decorate a typed function with `@tool`:
|
|
84
106
|
|
|
85
107
|
```python
|
|
86
108
|
from mycode import Agent, tool
|
|
@@ -88,7 +110,11 @@ from mycode import Agent, tool
|
|
|
88
110
|
|
|
89
111
|
@tool
|
|
90
112
|
def greet(name: str) -> str:
|
|
91
|
-
"""Return a friendly greeting.
|
|
113
|
+
"""Return a friendly greeting.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name: Person name.
|
|
117
|
+
"""
|
|
92
118
|
|
|
93
119
|
return f"hello, {name}"
|
|
94
120
|
|
|
@@ -100,7 +126,7 @@ agent = Agent(
|
|
|
100
126
|
)
|
|
101
127
|
```
|
|
102
128
|
|
|
103
|
-
To call a built-in tool from inside your own, type the first parameter as `ToolContext`:
|
|
129
|
+
To call a built-in tool from inside your own tool, type the first parameter as `ToolContext`:
|
|
104
130
|
|
|
105
131
|
```python
|
|
106
132
|
from mycode import ToolContext, tool
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mycode-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.1"
|
|
8
8
|
description = "Lightweight Python SDK for building AI agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -23,9 +23,11 @@ classifiers = [
|
|
|
23
23
|
]
|
|
24
24
|
keywords = ["agent", "llm", "anthropic", "openai", "gemini", "sdk"]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"anthropic>=0.
|
|
27
|
-
"google-genai>=2.
|
|
28
|
-
"
|
|
26
|
+
"anthropic>=0.106.0",
|
|
27
|
+
"google-genai>=2.8.0",
|
|
28
|
+
"griffelib>=2.0.0",
|
|
29
|
+
"openai>=2.41.0",
|
|
30
|
+
"pydantic>=2.13.0",
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
[project.urls]
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
"""mycode — multi-turn tool-calling agent runtime.
|
|
2
|
-
|
|
3
|
-
Public API for embedding the agent loop in other Python applications. The
|
|
4
|
-
runtime ships four built-in coding tools (``read``, ``write``, ``edit``,
|
|
5
|
-
``bash``) exposed as :data:`read_tool`, :data:`write_tool`, :data:`edit_tool`,
|
|
6
|
-
:data:`bash_tool` — pick the ones you want via ``tools=[...]`` rather than
|
|
7
|
-
silently exposing file system and shell access.
|
|
8
|
-
"""
|
|
1
|
+
"""mycode — multi-turn tool-calling agent runtime."""
|
|
9
2
|
|
|
10
3
|
from importlib import metadata
|
|
11
4
|
|
|
12
5
|
from mycode.agent import Agent, Event, PersistCallback, RunResult
|
|
6
|
+
from mycode.attachments import Attachment
|
|
13
7
|
from mycode.hooks import AfterToolHook, BeforeToolHook, HookResult, Hooks, ToolHookContext
|
|
14
8
|
from mycode.messages import (
|
|
15
9
|
ContentBlock,
|
|
@@ -27,25 +21,25 @@ from mycode.messages import (
|
|
|
27
21
|
)
|
|
28
22
|
from mycode.session import SessionStore
|
|
29
23
|
from mycode.tools import (
|
|
30
|
-
DEFAULT_TOOL_SPECS,
|
|
31
24
|
ToolContext,
|
|
32
25
|
ToolExecutionResult,
|
|
33
26
|
ToolExecutor,
|
|
34
27
|
ToolSpec,
|
|
28
|
+
bash_tool,
|
|
35
29
|
cancel_all_tools,
|
|
30
|
+
edit_tool,
|
|
31
|
+
read_tool,
|
|
36
32
|
tool,
|
|
33
|
+
write_tool,
|
|
37
34
|
)
|
|
38
35
|
|
|
39
|
-
# The package metadata in mycode/pyproject.toml is the single version source.
|
|
40
36
|
__version__ = metadata.version("mycode-sdk")
|
|
41
37
|
|
|
42
|
-
read_tool, write_tool, edit_tool, bash_tool = DEFAULT_TOOL_SPECS
|
|
43
|
-
|
|
44
38
|
__all__ = [
|
|
45
39
|
"Agent",
|
|
40
|
+
"Attachment",
|
|
46
41
|
"ContentBlock",
|
|
47
42
|
"ConversationMessage",
|
|
48
|
-
"DEFAULT_TOOL_SPECS",
|
|
49
43
|
"Event",
|
|
50
44
|
"AfterToolHook",
|
|
51
45
|
"BeforeToolHook",
|
|
@@ -13,15 +13,16 @@ import os
|
|
|
13
13
|
import tempfile
|
|
14
14
|
import time
|
|
15
15
|
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
|
16
|
+
from contextlib import suppress
|
|
16
17
|
from dataclasses import dataclass, field
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any, cast
|
|
19
20
|
from uuid import uuid4
|
|
20
21
|
|
|
22
|
+
from mycode.attachments import AttachmentLike, build_attachment_blocks
|
|
21
23
|
from mycode.compact import (
|
|
22
24
|
COMPACT_SUMMARY_PROMPT,
|
|
23
25
|
DEFAULT_COMPACT_THRESHOLD,
|
|
24
|
-
apply_compact_replay,
|
|
25
26
|
build_compact_event,
|
|
26
27
|
should_compact,
|
|
27
28
|
)
|
|
@@ -79,6 +80,7 @@ class Agent:
|
|
|
79
80
|
messages: list[ConversationMessage] | None = None,
|
|
80
81
|
max_turns: int | None = None,
|
|
81
82
|
max_tokens: int | None = None,
|
|
83
|
+
temperature: float = 1.0,
|
|
82
84
|
context_window: int | None = None,
|
|
83
85
|
compact_threshold: float | None = None,
|
|
84
86
|
reasoning_effort: str | None = None,
|
|
@@ -111,6 +113,16 @@ class Agent:
|
|
|
111
113
|
self.api_key = api_key
|
|
112
114
|
self.api_base = api_base
|
|
113
115
|
self.max_turns = max_turns
|
|
116
|
+
if not 0 <= temperature <= 1:
|
|
117
|
+
raise ValueError("temperature must be between 0 and 1")
|
|
118
|
+
if (
|
|
119
|
+
provider in {"anthropic", "moonshotai", "minimax"}
|
|
120
|
+
and reasoning_effort
|
|
121
|
+
and reasoning_effort != "none"
|
|
122
|
+
and temperature != 1.0
|
|
123
|
+
):
|
|
124
|
+
raise ValueError(f"{provider} does not support custom temperature when thinking is enabled")
|
|
125
|
+
self.temperature = float(temperature)
|
|
114
126
|
self.compact_threshold = compact_threshold if compact_threshold is not None else DEFAULT_COMPACT_THRESHOLD
|
|
115
127
|
self.reasoning_effort = reasoning_effort
|
|
116
128
|
|
|
@@ -221,18 +233,12 @@ class Agent:
|
|
|
221
233
|
yield Event("tool_start", {"tool_call": {"id": tool_id, "name": name, "input": args}})
|
|
222
234
|
|
|
223
235
|
if self._cancel_event.is_set():
|
|
224
|
-
yield self.
|
|
225
|
-
tool_id,
|
|
226
|
-
ToolExecutionResult(output="error: cancelled", is_error=True),
|
|
227
|
-
)
|
|
236
|
+
yield self._error_done(tool_id, "error: cancelled")
|
|
228
237
|
return
|
|
229
238
|
|
|
230
239
|
spec = self.tools.get(name)
|
|
231
240
|
if spec is None:
|
|
232
|
-
yield self.
|
|
233
|
-
tool_id,
|
|
234
|
-
ToolExecutionResult(output=f"error: unknown tool: {name}", is_error=True),
|
|
235
|
-
)
|
|
241
|
+
yield self._error_done(tool_id, f"error: unknown tool: {name}")
|
|
236
242
|
return
|
|
237
243
|
|
|
238
244
|
hook_ctx = ToolHookContext(
|
|
@@ -248,10 +254,7 @@ class Agent:
|
|
|
248
254
|
try:
|
|
249
255
|
result = await self.hooks.run_before_tool(hook_ctx)
|
|
250
256
|
except Exception as exc:
|
|
251
|
-
yield self.
|
|
252
|
-
tool_id,
|
|
253
|
-
ToolExecutionResult(output=f"error: tool hook failed: {exc}", is_error=True),
|
|
254
|
-
)
|
|
257
|
+
yield self._error_done(tool_id, f"error: tool hook failed: {exc}")
|
|
255
258
|
return
|
|
256
259
|
|
|
257
260
|
if result is not None:
|
|
@@ -259,10 +262,7 @@ class Agent:
|
|
|
259
262
|
return
|
|
260
263
|
|
|
261
264
|
if self._cancel_event.is_set():
|
|
262
|
-
yield self.
|
|
263
|
-
tool_id,
|
|
264
|
-
ToolExecutionResult(output="error: cancelled", is_error=True),
|
|
265
|
-
)
|
|
265
|
+
yield self._error_done(tool_id, "error: cancelled")
|
|
266
266
|
return
|
|
267
267
|
|
|
268
268
|
if spec.streams_output:
|
|
@@ -325,13 +325,10 @@ class Agent:
|
|
|
325
325
|
yield Event("tool_output", {"tool_use_id": tool_id, "output": output})
|
|
326
326
|
|
|
327
327
|
if was_cancelled:
|
|
328
|
-
|
|
328
|
+
with suppress(Exception):
|
|
329
329
|
await task
|
|
330
|
-
except Exception:
|
|
331
|
-
pass
|
|
332
330
|
output = "\n".join([*output_parts, "error: cancelled"]) if output_parts else "error: cancelled"
|
|
333
|
-
|
|
334
|
-
yield self._tool_done_event(tool_id, result)
|
|
331
|
+
yield self._error_done(tool_id, output)
|
|
335
332
|
return
|
|
336
333
|
else:
|
|
337
334
|
try:
|
|
@@ -387,6 +384,11 @@ class Agent:
|
|
|
387
384
|
data["content"] = result.content
|
|
388
385
|
return Event("tool_done", data)
|
|
389
386
|
|
|
387
|
+
def _error_done(self, tool_id: str, message: str) -> Event:
|
|
388
|
+
"""Build a tool_done event carrying an error result."""
|
|
389
|
+
|
|
390
|
+
return self._tool_done_event(tool_id, ToolExecutionResult(output=message, is_error=True))
|
|
391
|
+
|
|
390
392
|
# ------------------------------------------------------------------
|
|
391
393
|
# Provider streaming
|
|
392
394
|
# ------------------------------------------------------------------
|
|
@@ -418,10 +420,52 @@ class Agent:
|
|
|
418
420
|
finally:
|
|
419
421
|
close = cast(Callable[[], Awaitable[None]] | None, getattr(provider_stream, "aclose", None))
|
|
420
422
|
if close is not None:
|
|
421
|
-
|
|
423
|
+
with suppress(Exception):
|
|
422
424
|
await close()
|
|
423
|
-
|
|
424
|
-
|
|
425
|
+
|
|
426
|
+
def _build_request(
|
|
427
|
+
self,
|
|
428
|
+
*,
|
|
429
|
+
tools: list[dict[str, Any]] | None = None,
|
|
430
|
+
max_tokens: int | None = None,
|
|
431
|
+
reasoning_effort: str | None = None,
|
|
432
|
+
append_messages: Sequence[ConversationMessage] = (),
|
|
433
|
+
) -> ProviderRequest:
|
|
434
|
+
"""Build a ProviderRequest from the agent's current state."""
|
|
435
|
+
|
|
436
|
+
return ProviderRequest(
|
|
437
|
+
provider=self.provider,
|
|
438
|
+
model=self.model,
|
|
439
|
+
session_id=self.session_id,
|
|
440
|
+
messages=self.messages,
|
|
441
|
+
system=self.system,
|
|
442
|
+
tools=self.tools.definitions if tools is None else tools,
|
|
443
|
+
max_tokens=self.max_tokens if max_tokens is None else max_tokens,
|
|
444
|
+
temperature=self.temperature,
|
|
445
|
+
api_key=self.api_key,
|
|
446
|
+
api_base=self.api_base,
|
|
447
|
+
reasoning_effort=reasoning_effort,
|
|
448
|
+
supports_image_input=self.supports_image_input,
|
|
449
|
+
supports_pdf_input=self.supports_pdf_input,
|
|
450
|
+
transcript_path=self.transcript_path,
|
|
451
|
+
append_messages=list(append_messages),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
@staticmethod
|
|
455
|
+
def _elapsed_ms(start: float) -> int:
|
|
456
|
+
return max(0, int((time.monotonic() - start) * 1000))
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def _stamp_thinking_duration(content: list[Any], duration_ms: int) -> None:
|
|
460
|
+
"""Merge duration_ms onto the last thinking block in content, if any."""
|
|
461
|
+
|
|
462
|
+
for block in reversed(content):
|
|
463
|
+
if not isinstance(block, dict) or block.get("type") != "thinking":
|
|
464
|
+
continue
|
|
465
|
+
raw_meta = block.get("meta")
|
|
466
|
+
meta = raw_meta if isinstance(raw_meta, dict) else {}
|
|
467
|
+
block["meta"] = {**meta, "duration_ms": duration_ms}
|
|
468
|
+
return
|
|
425
469
|
|
|
426
470
|
# ------------------------------------------------------------------
|
|
427
471
|
# Public entry points
|
|
@@ -431,6 +475,7 @@ class Agent:
|
|
|
431
475
|
self,
|
|
432
476
|
user_input: str | ConversationMessage,
|
|
433
477
|
*,
|
|
478
|
+
attachments: Sequence[AttachmentLike] = (),
|
|
434
479
|
on_persist: PersistCallback | None = None,
|
|
435
480
|
) -> AsyncIterator[Event]:
|
|
436
481
|
"""Run the full agent loop for one user message."""
|
|
@@ -448,8 +493,9 @@ class Agent:
|
|
|
448
493
|
|
|
449
494
|
self._cancel_event.clear()
|
|
450
495
|
|
|
496
|
+
user_message: ConversationMessage
|
|
451
497
|
if isinstance(user_input, str):
|
|
452
|
-
user_message
|
|
498
|
+
user_message = user_text_message(user_input)
|
|
453
499
|
else:
|
|
454
500
|
if (user_input.get("role") or "user") != "user":
|
|
455
501
|
yield Event("error", {"message": "user input must be a user message"})
|
|
@@ -462,17 +508,20 @@ class Agent:
|
|
|
462
508
|
if isinstance(raw_meta, dict):
|
|
463
509
|
user_message["meta"] = {str(k): v for k, v in raw_meta.items()}
|
|
464
510
|
|
|
511
|
+
if attachments:
|
|
512
|
+
blocks = await asyncio.to_thread(build_attachment_blocks, attachments, cwd=self.cwd)
|
|
513
|
+
user_message["content"] = list(user_message.get("content") or []) + blocks
|
|
514
|
+
|
|
465
515
|
content_blocks = user_message.get("content") or []
|
|
466
|
-
|
|
467
|
-
|
|
516
|
+
for block_type, supported, label in (
|
|
517
|
+
("image", self.supports_image_input, "image input"),
|
|
518
|
+
("document", self.supports_pdf_input, "PDF input"),
|
|
468
519
|
):
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
yield Event("error", {"message": "current model does not support PDF input"})
|
|
475
|
-
return
|
|
520
|
+
if not supported and any(
|
|
521
|
+
isinstance(block, dict) and block.get("type") == block_type for block in content_blocks
|
|
522
|
+
):
|
|
523
|
+
yield Event("error", {"message": f"current model does not support {label}"})
|
|
524
|
+
return
|
|
476
525
|
|
|
477
526
|
self.messages.append(user_message)
|
|
478
527
|
await persist(user_message)
|
|
@@ -491,20 +540,7 @@ class Agent:
|
|
|
491
540
|
thinking_started_at: float | None = None
|
|
492
541
|
thinking_duration_ms: int | None = None
|
|
493
542
|
provider_cancelled = False
|
|
494
|
-
request =
|
|
495
|
-
provider=self.provider,
|
|
496
|
-
model=self.model,
|
|
497
|
-
session_id=self.session_id,
|
|
498
|
-
messages=apply_compact_replay(self.messages, transcript_path=self.transcript_path),
|
|
499
|
-
system=self.system,
|
|
500
|
-
tools=self.tools.definitions,
|
|
501
|
-
max_tokens=self.max_tokens,
|
|
502
|
-
api_key=self.api_key,
|
|
503
|
-
api_base=self.api_base,
|
|
504
|
-
reasoning_effort=self.reasoning_effort,
|
|
505
|
-
supports_image_input=self.supports_image_input,
|
|
506
|
-
supports_pdf_input=self.supports_pdf_input,
|
|
507
|
-
)
|
|
543
|
+
request = self._build_request(reasoning_effort=self.reasoning_effort)
|
|
508
544
|
|
|
509
545
|
try:
|
|
510
546
|
async for provider_event in self._stream_provider_turn(adapter, request):
|
|
@@ -528,7 +564,7 @@ class Agent:
|
|
|
528
564
|
delta_text = str(provider_event.data.get("text") or "")
|
|
529
565
|
if delta_text:
|
|
530
566
|
if thinking_started_at is not None and thinking_duration_ms is None:
|
|
531
|
-
thinking_duration_ms =
|
|
567
|
+
thinking_duration_ms = self._elapsed_ms(thinking_started_at)
|
|
532
568
|
yield Event("reasoning_done", {"duration_ms": thinking_duration_ms})
|
|
533
569
|
if partial_content and partial_content[-1].get("type") == "text":
|
|
534
570
|
partial_content[-1]["text"] = f"{partial_content[-1].get('text') or ''}{delta_text}"
|
|
@@ -544,7 +580,7 @@ class Agent:
|
|
|
544
580
|
continue
|
|
545
581
|
|
|
546
582
|
if thinking_started_at is not None and thinking_duration_ms is None:
|
|
547
|
-
thinking_duration_ms =
|
|
583
|
+
thinking_duration_ms = self._elapsed_ms(thinking_started_at)
|
|
548
584
|
yield Event("reasoning_done", {"duration_ms": thinking_duration_ms})
|
|
549
585
|
|
|
550
586
|
message = provider_event.data.get("message")
|
|
@@ -561,15 +597,9 @@ class Agent:
|
|
|
561
597
|
if provider_cancelled:
|
|
562
598
|
if partial_content:
|
|
563
599
|
if thinking_started_at is not None and thinking_duration_ms is None:
|
|
564
|
-
thinking_duration_ms =
|
|
600
|
+
thinking_duration_ms = self._elapsed_ms(thinking_started_at)
|
|
565
601
|
if thinking_duration_ms is not None:
|
|
566
|
-
|
|
567
|
-
if block.get("type") != "thinking":
|
|
568
|
-
continue
|
|
569
|
-
raw_meta = block.get("meta")
|
|
570
|
-
meta = cast(dict[str, Any], raw_meta) if isinstance(raw_meta, dict) else {}
|
|
571
|
-
block["meta"] = {**meta, "duration_ms": thinking_duration_ms}
|
|
572
|
-
break
|
|
602
|
+
self._stamp_thinking_duration(partial_content, thinking_duration_ms)
|
|
573
603
|
cancelled_message = build_message(
|
|
574
604
|
"assistant",
|
|
575
605
|
[dict(block) for block in partial_content],
|
|
@@ -589,13 +619,7 @@ class Agent:
|
|
|
589
619
|
return
|
|
590
620
|
|
|
591
621
|
if thinking_duration_ms is not None:
|
|
592
|
-
|
|
593
|
-
if not isinstance(block, dict) or block.get("type") != "thinking":
|
|
594
|
-
continue
|
|
595
|
-
raw_meta = block.get("meta")
|
|
596
|
-
meta = cast(dict[str, Any], raw_meta) if isinstance(raw_meta, dict) else {}
|
|
597
|
-
block["meta"] = {**meta, "duration_ms": thinking_duration_ms}
|
|
598
|
-
break
|
|
622
|
+
self._stamp_thinking_duration(assistant_message.get("content") or [], thinking_duration_ms)
|
|
599
623
|
|
|
600
624
|
# Stamp context_window onto the persisted assistant message so
|
|
601
625
|
# rewinds and refreshed clients can render token-usage % without
|
|
@@ -631,16 +655,13 @@ class Agent:
|
|
|
631
655
|
continue
|
|
632
656
|
|
|
633
657
|
d = event.data
|
|
634
|
-
output = str(d.get("output") or "")
|
|
635
|
-
metadata = d.get("metadata") if isinstance(d.get("metadata"), dict) else None
|
|
636
|
-
content = d.get("content")
|
|
637
658
|
tool_results.append(
|
|
638
659
|
tool_result_block(
|
|
639
|
-
tool_use_id=
|
|
640
|
-
output=output,
|
|
641
|
-
metadata=metadata,
|
|
642
|
-
is_error=
|
|
643
|
-
content=
|
|
660
|
+
tool_use_id=d["tool_use_id"],
|
|
661
|
+
output=d["output"],
|
|
662
|
+
metadata=d.get("metadata"),
|
|
663
|
+
is_error=d["is_error"],
|
|
664
|
+
content=d.get("content"),
|
|
644
665
|
)
|
|
645
666
|
)
|
|
646
667
|
|
|
@@ -682,6 +703,7 @@ class Agent:
|
|
|
682
703
|
self,
|
|
683
704
|
user_input: str | ConversationMessage,
|
|
684
705
|
*,
|
|
706
|
+
attachments: Sequence[AttachmentLike] = (),
|
|
685
707
|
on_persist: PersistCallback | None = None,
|
|
686
708
|
) -> RunResult:
|
|
687
709
|
"""Run one user turn synchronously and collect the streamed result."""
|
|
@@ -695,7 +717,7 @@ class Agent:
|
|
|
695
717
|
|
|
696
718
|
async def collect() -> RunResult:
|
|
697
719
|
result = RunResult()
|
|
698
|
-
async for event in self.achat(user_input, on_persist=on_persist):
|
|
720
|
+
async for event in self.achat(user_input, attachments=attachments, on_persist=on_persist):
|
|
699
721
|
result.events.append(event)
|
|
700
722
|
if event.type == "text":
|
|
701
723
|
result.text += str(event.data.get("delta") or "")
|
|
@@ -716,21 +738,10 @@ class Agent:
|
|
|
716
738
|
) -> None:
|
|
717
739
|
"""Ask the provider for a summary, persist the compact event, append it."""
|
|
718
740
|
|
|
719
|
-
|
|
720
|
-
user_text_message(COMPACT_SUMMARY_PROMPT)
|
|
721
|
-
]
|
|
722
|
-
request = ProviderRequest(
|
|
723
|
-
provider=self.provider,
|
|
724
|
-
model=self.model,
|
|
725
|
-
session_id=self.session_id,
|
|
726
|
-
messages=compact_messages,
|
|
727
|
-
system=self.system,
|
|
741
|
+
request = self._build_request(
|
|
728
742
|
tools=[],
|
|
729
743
|
max_tokens=min(self.max_tokens, 8192),
|
|
730
|
-
|
|
731
|
-
api_base=self.api_base,
|
|
732
|
-
supports_image_input=self.supports_image_input,
|
|
733
|
-
supports_pdf_input=self.supports_pdf_input,
|
|
744
|
+
append_messages=[user_text_message(COMPACT_SUMMARY_PROMPT)],
|
|
734
745
|
)
|
|
735
746
|
|
|
736
747
|
summary_message: ConversationMessage | None = None
|