mycode-sdk 0.13.0__tar.gz → 0.13.2__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.13.0 → mycode_sdk-0.13.2}/PKG-INFO +1 -1
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/pyproject.toml +1 -1
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/agent.py +12 -28
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/attachments.py +7 -10
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/hooks.py +12 -9
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/messages.py +10 -13
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/models_catalog.json +911 -552
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/gemini.py +1 -1
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/openai_chat.py +3 -5
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/openai_responses.py +12 -15
- mycode_sdk-0.13.2/src/mycode/session.py +153 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/tools.py +17 -12
- mycode_sdk-0.13.0/src/mycode/session.py +0 -319
- mycode_sdk-0.13.0/src/mycode/utils.py +0 -20
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/.gitignore +0 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/LICENSE +0 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/README.md +0 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/__init__.py +0 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/compact.py +0 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/models.py +0 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/anthropic_like.py +0 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/base.py +0 -0
- {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/py.typed +0 -0
|
@@ -9,9 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import logging
|
|
12
|
-
import os
|
|
13
12
|
import random
|
|
14
|
-
import tempfile
|
|
15
13
|
import threading
|
|
16
14
|
import time
|
|
17
15
|
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
|
@@ -204,7 +202,6 @@ class Agent:
|
|
|
204
202
|
self,
|
|
205
203
|
*,
|
|
206
204
|
model: str,
|
|
207
|
-
cwd: str | None = None,
|
|
208
205
|
provider: str | None = None,
|
|
209
206
|
session_dir: Path | None = None,
|
|
210
207
|
session_id: str | None = None,
|
|
@@ -227,6 +224,7 @@ class Agent:
|
|
|
227
224
|
system: str = "",
|
|
228
225
|
tools: Sequence[ToolSpec] = (),
|
|
229
226
|
hooks: Hooks | None = None,
|
|
227
|
+
deps: object | None = None,
|
|
230
228
|
):
|
|
231
229
|
self.model = model
|
|
232
230
|
if provider is None:
|
|
@@ -236,8 +234,9 @@ class Agent:
|
|
|
236
234
|
provider = inferred
|
|
237
235
|
self.provider = provider
|
|
238
236
|
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
# Opaque application context handed to every tool and hook; the SDK
|
|
238
|
+
# never reads it.
|
|
239
|
+
self.deps = deps
|
|
241
240
|
|
|
242
241
|
# Persistence is opt-in: a store is only created when ``session_dir``
|
|
243
242
|
# is supplied. ``session_id`` is always populated (uuid when absent)
|
|
@@ -286,11 +285,7 @@ class Agent:
|
|
|
286
285
|
# - messages is None → auto-resume from disk if the session exists
|
|
287
286
|
# - messages is [] or [...] → use as-is; refuse if it would overwrite disk
|
|
288
287
|
if messages is None:
|
|
289
|
-
if self._store is not None
|
|
290
|
-
data = self._store.load_session_sync(self.session_id)
|
|
291
|
-
messages = list(data["messages"]) if data is not None else []
|
|
292
|
-
else:
|
|
293
|
-
messages = []
|
|
288
|
+
messages = self._store.load_messages_sync(self.session_id) if self._store is not None else []
|
|
294
289
|
elif self._store is not None and self._store.session_exists(self.session_id):
|
|
295
290
|
msg = (
|
|
296
291
|
f"session {self.session_id!r} already exists on disk; "
|
|
@@ -299,13 +294,6 @@ class Agent:
|
|
|
299
294
|
raise ValueError(msg)
|
|
300
295
|
self.messages: list[ConversationMessage] = list(messages)
|
|
301
296
|
|
|
302
|
-
# ``tool_output_dir`` defaults to a session-adjacent directory so logs
|
|
303
|
-
# (e.g. bash spill files) live next to the session JSONL; without a
|
|
304
|
-
# session, fall back to a tempdir scoped to ``session_id``.
|
|
305
|
-
if session_dir is not None:
|
|
306
|
-
self.tool_output_dir = session_dir / self.session_id / "tool-output"
|
|
307
|
-
else:
|
|
308
|
-
self.tool_output_dir = Path(tempfile.gettempdir()) / "mycode" / self.session_id / "tool-output"
|
|
309
297
|
self.tools = ToolExecutor(tools)
|
|
310
298
|
|
|
311
299
|
self.refresh_capabilities(
|
|
@@ -382,7 +370,7 @@ class Agent:
|
|
|
382
370
|
self,
|
|
383
371
|
spec: ToolSpec,
|
|
384
372
|
args: dict[str, Any],
|
|
385
|
-
ctx: ToolContext,
|
|
373
|
+
ctx: ToolContext[Any],
|
|
386
374
|
) -> ToolExecutionResult:
|
|
387
375
|
return await self.tools.aexecute(spec.name, args, ctx)
|
|
388
376
|
|
|
@@ -430,8 +418,7 @@ class Agent:
|
|
|
430
418
|
|
|
431
419
|
hook_ctx = ToolHookContext(
|
|
432
420
|
session_id=self.session_id,
|
|
433
|
-
|
|
434
|
-
tool_output_dir=self.tool_output_dir,
|
|
421
|
+
deps=self.deps,
|
|
435
422
|
provider=self.provider,
|
|
436
423
|
model=self.model,
|
|
437
424
|
tool_call_id=tool_id,
|
|
@@ -488,7 +475,7 @@ class Agent:
|
|
|
488
475
|
tool_id: str,
|
|
489
476
|
spec: ToolSpec,
|
|
490
477
|
args: dict[str, Any],
|
|
491
|
-
hook_ctx: ToolHookContext,
|
|
478
|
+
hook_ctx: ToolHookContext[Any],
|
|
492
479
|
) -> AsyncIterator[Event]:
|
|
493
480
|
"""Run one streaming tool, forwarding ``tool_output`` events live."""
|
|
494
481
|
|
|
@@ -545,7 +532,7 @@ class Agent:
|
|
|
545
532
|
async def _finish_tool_call(
|
|
546
533
|
self,
|
|
547
534
|
tool_id: str,
|
|
548
|
-
hook_ctx: ToolHookContext,
|
|
535
|
+
hook_ctx: ToolHookContext[Any],
|
|
549
536
|
result: ToolExecutionResult,
|
|
550
537
|
) -> Event:
|
|
551
538
|
try:
|
|
@@ -563,11 +550,10 @@ class Agent:
|
|
|
563
550
|
tool_id: str,
|
|
564
551
|
*,
|
|
565
552
|
emit: Callable[[str], None] | None = None,
|
|
566
|
-
) -> ToolContext:
|
|
553
|
+
) -> ToolContext[Any]:
|
|
567
554
|
return ToolContext(
|
|
568
555
|
executor=self.tools,
|
|
569
|
-
|
|
570
|
-
tool_output_dir=self.tool_output_dir,
|
|
556
|
+
deps=self.deps,
|
|
571
557
|
supports_image_input=self.supports_image_input,
|
|
572
558
|
tool_call_id=tool_id,
|
|
573
559
|
emit=emit,
|
|
@@ -811,8 +797,6 @@ class Agent:
|
|
|
811
797
|
await on_persist(message)
|
|
812
798
|
if self._store is None:
|
|
813
799
|
return
|
|
814
|
-
if not self._store.session_exists(self.session_id):
|
|
815
|
-
await self._store.create_session(self.session_id, cwd=self.cwd)
|
|
816
800
|
await self._store.append_message(self.session_id, message)
|
|
817
801
|
|
|
818
802
|
# ------------------------------------------------------------------
|
|
@@ -850,7 +834,7 @@ class Agent:
|
|
|
850
834
|
user_message["meta"] = {str(k): v for k, v in raw_meta.items()}
|
|
851
835
|
|
|
852
836
|
if attachments:
|
|
853
|
-
blocks = await asyncio.to_thread(build_attachment_blocks, attachments
|
|
837
|
+
blocks = await asyncio.to_thread(build_attachment_blocks, attachments)
|
|
854
838
|
user_message["content"] = list(user_message.get("content") or []) + blocks
|
|
855
839
|
|
|
856
840
|
content_blocks = user_message.get("content") or []
|
|
@@ -16,7 +16,6 @@ from pathlib import Path
|
|
|
16
16
|
from typing import Self
|
|
17
17
|
|
|
18
18
|
from mycode.messages import ContentBlock, document_block, image_block, text_block
|
|
19
|
-
from mycode.utils import resolve_path
|
|
20
19
|
|
|
21
20
|
SUPPORTED_IMAGE_MIME_TYPES = frozenset({"image/png", "image/jpeg", "image/gif", "image/webp"})
|
|
22
21
|
SUPPORTED_DOCUMENT_MIME_TYPES = frozenset({"application/pdf"})
|
|
@@ -60,16 +59,14 @@ class Attachment:
|
|
|
60
59
|
AttachmentLike = str | Path | Attachment
|
|
61
60
|
|
|
62
61
|
|
|
63
|
-
def build_attachment_blocks(
|
|
64
|
-
attachments: Sequence[AttachmentLike],
|
|
65
|
-
*,
|
|
66
|
-
cwd: str,
|
|
67
|
-
) -> list[ContentBlock]:
|
|
62
|
+
def build_attachment_blocks(attachments: Sequence[AttachmentLike]) -> list[ContentBlock]:
|
|
68
63
|
"""Return one content block per attachment, in input order.
|
|
69
64
|
|
|
70
|
-
``str`` / ``Path`` items are treated as ``Attachment.path
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
``str`` / ``Path`` items are treated as ``Attachment.path``; ``~`` is
|
|
66
|
+
expanded and relative paths resolve against the process working
|
|
67
|
+
directory, like any other file API. Raises ``ValueError`` on a missing
|
|
68
|
+
path, a directory, a binary file that is neither image nor PDF,
|
|
69
|
+
undecodable text, or an unsupported ``media_type``.
|
|
73
70
|
"""
|
|
74
71
|
|
|
75
72
|
blocks: list[ContentBlock] = []
|
|
@@ -86,7 +83,7 @@ def build_attachment_blocks(
|
|
|
86
83
|
supported = sorted(SUPPORTED_IMAGE_MIME_TYPES | SUPPORTED_DOCUMENT_MIME_TYPES)
|
|
87
84
|
raise ValueError(f"unsupported media_type {media_type!r}; want one of {supported}")
|
|
88
85
|
case Path() as raw:
|
|
89
|
-
path =
|
|
86
|
+
path = raw.expanduser()
|
|
90
87
|
if not path.exists():
|
|
91
88
|
raise ValueError(f"attachment not found: {raw}")
|
|
92
89
|
if path.is_dir():
|
|
@@ -5,24 +5,27 @@ from __future__ import annotations
|
|
|
5
5
|
import inspect
|
|
6
6
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from pathlib import Path
|
|
9
8
|
from types import MappingProxyType
|
|
10
9
|
from typing import Any
|
|
11
10
|
|
|
12
11
|
from mycode.tools import ToolExecutionResult, ToolSpec
|
|
13
12
|
|
|
14
13
|
type HookResult = ToolExecutionResult | None
|
|
15
|
-
type BeforeToolHook = Callable[["ToolHookContext"], HookResult | Awaitable[HookResult]]
|
|
16
|
-
type AfterToolHook = Callable[["ToolHookContext", ToolExecutionResult], HookResult | Awaitable[HookResult]]
|
|
14
|
+
type BeforeToolHook = Callable[["ToolHookContext[Any]"], HookResult | Awaitable[HookResult]]
|
|
15
|
+
type AfterToolHook = Callable[["ToolHookContext[Any]", ToolExecutionResult], HookResult | Awaitable[HookResult]]
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
@dataclass(frozen=True)
|
|
20
|
-
class ToolHookContext:
|
|
21
|
-
"""Read-only context passed to tool execution hooks.
|
|
19
|
+
class ToolHookContext[DepsT]:
|
|
20
|
+
"""Read-only context passed to tool execution hooks.
|
|
21
|
+
|
|
22
|
+
``deps`` is the same application context object handed to tools via
|
|
23
|
+
``ToolContext.deps``; annotate hooks as ``ToolHookContext[MyDeps]`` for
|
|
24
|
+
typed access.
|
|
25
|
+
"""
|
|
22
26
|
|
|
23
27
|
session_id: str
|
|
24
|
-
|
|
25
|
-
tool_output_dir: Path
|
|
28
|
+
deps: DepsT
|
|
26
29
|
provider: str
|
|
27
30
|
model: str
|
|
28
31
|
tool_call_id: str
|
|
@@ -58,14 +61,14 @@ class Hooks:
|
|
|
58
61
|
self._after_tool.append(hook)
|
|
59
62
|
return hook
|
|
60
63
|
|
|
61
|
-
async def run_before_tool(self, ctx: ToolHookContext) -> ToolExecutionResult | None:
|
|
64
|
+
async def run_before_tool(self, ctx: ToolHookContext[Any]) -> ToolExecutionResult | None:
|
|
62
65
|
for hook in self._before_tool:
|
|
63
66
|
result = await _resolve(hook(ctx))
|
|
64
67
|
if result is not None:
|
|
65
68
|
return result
|
|
66
69
|
return None
|
|
67
70
|
|
|
68
|
-
async def run_after_tool(self, ctx: ToolHookContext, result: ToolExecutionResult) -> ToolExecutionResult:
|
|
71
|
+
async def run_after_tool(self, ctx: ToolHookContext[Any], result: ToolExecutionResult) -> ToolExecutionResult:
|
|
69
72
|
for hook in self._after_tool:
|
|
70
73
|
replacement = await _resolve(hook(ctx, result))
|
|
71
74
|
if replacement is not None:
|
|
@@ -19,8 +19,6 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
|
-
from mycode.utils import omit_none
|
|
23
|
-
|
|
24
22
|
ContentBlock = dict[str, Any]
|
|
25
23
|
ConversationMessage = dict[str, Any]
|
|
26
24
|
|
|
@@ -155,16 +153,15 @@ def build_usage(
|
|
|
155
153
|
|
|
156
154
|
if total_tokens is None and input_tokens is not None and output_tokens is not None:
|
|
157
155
|
total_tokens = input_tokens + output_tokens
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
156
|
+
usage = {
|
|
157
|
+
"total_tokens": total_tokens,
|
|
158
|
+
"input_tokens": input_tokens,
|
|
159
|
+
"cache_read_tokens": cache_read_tokens,
|
|
160
|
+
"cache_write_tokens": cache_write_tokens,
|
|
161
|
+
"output_tokens": output_tokens,
|
|
162
|
+
"reasoning_tokens": reasoning_tokens,
|
|
163
|
+
}
|
|
164
|
+
return {key: value for key, value in usage.items() if value is not None}
|
|
168
165
|
|
|
169
166
|
|
|
170
167
|
def assistant_message(
|
|
@@ -194,7 +191,7 @@ def assistant_message(
|
|
|
194
191
|
if cost is not None:
|
|
195
192
|
meta["cost"] = dict(cost)
|
|
196
193
|
if native_meta:
|
|
197
|
-
native =
|
|
194
|
+
native = {key: value for key, value in native_meta.items() if value is not None}
|
|
198
195
|
if native:
|
|
199
196
|
meta["native"] = native
|
|
200
197
|
return build_message("assistant", blocks, meta=meta or None)
|