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.
Files changed (24) hide show
  1. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/PKG-INFO +1 -1
  2. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/pyproject.toml +1 -1
  3. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/agent.py +12 -28
  4. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/attachments.py +7 -10
  5. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/hooks.py +12 -9
  6. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/messages.py +10 -13
  7. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/models_catalog.json +911 -552
  8. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/gemini.py +1 -1
  9. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/openai_chat.py +3 -5
  10. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/openai_responses.py +12 -15
  11. mycode_sdk-0.13.2/src/mycode/session.py +153 -0
  12. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/tools.py +17 -12
  13. mycode_sdk-0.13.0/src/mycode/session.py +0 -319
  14. mycode_sdk-0.13.0/src/mycode/utils.py +0 -20
  15. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/.gitignore +0 -0
  16. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/LICENSE +0 -0
  17. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/README.md +0 -0
  18. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/__init__.py +0 -0
  19. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/compact.py +0 -0
  20. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/models.py +0 -0
  21. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/__init__.py +0 -0
  22. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/anthropic_like.py +0 -0
  23. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/providers/base.py +0 -0
  24. {mycode_sdk-0.13.0 → mycode_sdk-0.13.2}/src/mycode/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mycode-sdk
3
- Version: 0.13.0
3
+ Version: 0.13.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mycode-sdk"
7
- version = "0.13.0"
7
+ version = "0.13.2"
8
8
  description = "Lightweight Python SDK for building AI agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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
- resolved_cwd = cwd if cwd is not None else os.getcwd()
240
- self.cwd = str(Path(resolved_cwd).resolve(strict=False))
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
- cwd=self.cwd,
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
- cwd=self.cwd,
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, cwd=self.cwd)
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``. Raises
71
- ``ValueError`` on a missing path, a directory, a binary file that is
72
- neither image nor PDF, undecodable text, or an unsupported ``media_type``.
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 = resolve_path(raw, cwd=cwd)
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
- cwd: str
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
- return omit_none(
159
- {
160
- "total_tokens": total_tokens,
161
- "input_tokens": input_tokens,
162
- "cache_read_tokens": cache_read_tokens,
163
- "cache_write_tokens": cache_write_tokens,
164
- "output_tokens": output_tokens,
165
- "reasoning_tokens": reasoning_tokens,
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 = omit_none(native_meta)
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)