mycode-sdk 0.9.3__tar.gz → 0.9.5__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 (23) hide show
  1. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/PKG-INFO +6 -6
  2. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/pyproject.toml +7 -7
  3. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/agent.py +29 -41
  4. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/messages.py +13 -22
  5. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/models.py +14 -4
  6. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/models_catalog.json +243 -215
  7. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/providers/anthropic_like.py +6 -15
  8. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/providers/base.py +20 -0
  9. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/providers/openai_chat.py +15 -27
  10. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/providers/openai_responses.py +5 -11
  11. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/session.py +10 -23
  12. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/tools.py +10 -24
  13. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/utils.py +0 -16
  14. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/.gitignore +0 -0
  15. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/LICENSE +0 -0
  16. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/README.md +0 -0
  17. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/__init__.py +0 -0
  18. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/attachments.py +0 -0
  19. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/compact.py +0 -0
  20. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/hooks.py +0 -0
  21. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/providers/__init__.py +0 -0
  22. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/providers/gemini.py +0 -0
  23. {mycode_sdk-0.9.3 → mycode_sdk-0.9.5}/src/mycode/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycode-sdk
3
- Version: 0.9.3
3
+ Version: 0.9.5
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,11 +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.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
21
+ Requires-Dist: anthropic>=0.116.0
22
+ Requires-Dist: google-genai>=2.11.0
23
+ Requires-Dist: griffelib>=2.1.0
24
+ Requires-Dist: openai>=2.45.0
25
+ Requires-Dist: pydantic>=2.13.4
26
26
  Description-Content-Type: text/markdown
27
27
 
28
28
  # mycode-sdk
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
2
+ requires = ["hatchling>=1.31.0"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mycode-sdk"
7
- version = "0.9.3"
7
+ version = "0.9.5"
8
8
  description = "Lightweight Python SDK for building AI agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -23,11 +23,11 @@ classifiers = [
23
23
  ]
24
24
  keywords = ["agent", "llm", "anthropic", "openai", "gemini", "sdk"]
25
25
  dependencies = [
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",
26
+ "anthropic>=0.116.0",
27
+ "google-genai>=2.11.0",
28
+ "griffelib>=2.1.0",
29
+ "openai>=2.45.0",
30
+ "pydantic>=2.13.4",
31
31
  ]
32
32
 
33
33
  [project.urls]
@@ -152,16 +152,10 @@ class Agent:
152
152
  # (e.g. bash spill files) live next to the session JSONL; without a
153
153
  # session, fall back to a tempdir scoped to ``session_id``.
154
154
  if session_dir is not None:
155
- tool_output_dir = session_dir / self.session_id / "tool-output"
155
+ self.tool_output_dir = session_dir / self.session_id / "tool-output"
156
156
  else:
157
- tool_output_dir = Path(tempfile.gettempdir()) / "mycode" / self.session_id / "tool-output"
157
+ self.tool_output_dir = Path(tempfile.gettempdir()) / "mycode" / self.session_id / "tool-output"
158
158
  self.tools = ToolExecutor(tools)
159
- self.tool_ctx = ToolContext(
160
- executor=self.tools,
161
- cwd=self.cwd,
162
- tool_output_dir=tool_output_dir,
163
- supports_image_input=False,
164
- )
165
159
 
166
160
  self.refresh_capabilities(
167
161
  max_tokens=max_tokens,
@@ -201,7 +195,6 @@ class Agent:
201
195
  self.supports_reasoning: bool | None = meta.supports_reasoning
202
196
  self.supports_image_input: bool = bool(meta.supports_image_input)
203
197
  self.supports_pdf_input: bool = bool(meta.supports_pdf_input)
204
- self.tool_ctx.supports_image_input = self.supports_image_input
205
198
 
206
199
  def cancel(self) -> None:
207
200
  """Request cancellation of the in-flight turn."""
@@ -330,11 +323,11 @@ class Agent:
330
323
  output = "\n".join([*output_parts, "error: cancelled"]) if output_parts else "error: cancelled"
331
324
  yield self._error_done(tool_id, output)
332
325
  return
333
- else:
334
- try:
335
- result = await task
336
- except Exception as exc: # pragma: no cover - defensive
337
- result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
326
+
327
+ try:
328
+ result = await task
329
+ except Exception as exc: # pragma: no cover - defensive
330
+ result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
338
331
 
339
332
  yield await self._finish_tool_call(tool_id, hook_ctx, result)
340
333
 
@@ -360,13 +353,11 @@ class Agent:
360
353
  *,
361
354
  emit: Callable[[str], None] | None = None,
362
355
  ) -> ToolContext:
363
- """Build a per-call ToolContext from the base context."""
364
-
365
356
  return ToolContext(
366
357
  executor=self.tools,
367
- cwd=self.tool_ctx.cwd,
368
- tool_output_dir=self.tool_ctx.tool_output_dir,
369
- supports_image_input=self.tool_ctx.supports_image_input,
358
+ cwd=self.cwd,
359
+ tool_output_dir=self.tool_output_dir,
360
+ supports_image_input=self.supports_image_input,
370
361
  tool_call_id=tool_id,
371
362
  emit=emit,
372
363
  )
@@ -402,15 +393,12 @@ class Agent:
402
393
 
403
394
  provider_stream: AsyncIterator[ProviderStreamEvent] = adapter.stream_turn(request)
404
395
 
405
- async def next_provider_event() -> ProviderStreamEvent:
406
- return await anext(provider_stream)
407
-
408
396
  try:
409
397
  while True:
410
398
  if self._cancel_event.is_set():
411
399
  raise asyncio.CancelledError
412
400
 
413
- self._provider_event_task = asyncio.create_task(next_provider_event())
401
+ self._provider_event_task = asyncio.ensure_future(anext(provider_stream))
414
402
  try:
415
403
  yield await self._provider_event_task
416
404
  except StopAsyncIteration:
@@ -529,7 +517,10 @@ class Agent:
529
517
  adapter = get_provider_adapter(self.provider)
530
518
 
531
519
  turn_number = 0
532
- while self.max_turns is None or turn_number < self.max_turns:
520
+ while True:
521
+ if self.max_turns is not None and turn_number >= self.max_turns:
522
+ yield Event("error", {"message": "max_turns reached"})
523
+ return
533
524
  turn_number += 1
534
525
  if self._cancel_event.is_set():
535
526
  yield Event("error", {"message": "cancelled"})
@@ -632,13 +623,15 @@ class Agent:
632
623
 
633
624
  total_tokens = meta.get("total_tokens")
634
625
  if total_tokens:
635
- payload: dict[str, Any] = {
636
- "total_tokens": total_tokens,
637
- "model": meta.get("model") or self.model,
638
- "provider": meta.get("provider") or self.provider,
639
- "context_window": meta["context_window"],
640
- }
641
- yield Event("usage", payload)
626
+ yield Event(
627
+ "usage",
628
+ {
629
+ "total_tokens": total_tokens,
630
+ "model": meta.get("model") or self.model,
631
+ "provider": meta.get("provider") or self.provider,
632
+ "context_window": meta["context_window"],
633
+ },
634
+ )
642
635
 
643
636
  tool_calls = [
644
637
  block
@@ -665,11 +658,10 @@ class Agent:
665
658
  )
666
659
  )
667
660
 
668
- if self._cancel_event.is_set():
669
- tool_result_message = build_message("user", tool_results)
670
- self.messages.append(tool_result_message)
671
- await persist(tool_result_message)
672
- return
661
+ if self._cancel_event.is_set():
662
+ # Skip remaining tool calls; the results collected so
663
+ # far are still persisted below.
664
+ break
673
665
 
674
666
  tool_result_message = build_message("user", tool_results)
675
667
  self.messages.append(tool_result_message)
@@ -693,11 +685,7 @@ class Agent:
693
685
  )
694
686
 
695
687
  if not tool_calls:
696
- break
697
-
698
- else:
699
- yield Event("error", {"message": "max_turns reached"})
700
- return
688
+ return
701
689
 
702
690
  def run(
703
691
  self,
@@ -25,18 +25,20 @@ ContentBlock = dict[str, Any]
25
25
  ConversationMessage = dict[str, Any]
26
26
 
27
27
 
28
- def text_block(text: str, *, meta: dict[str, Any] | None = None) -> ContentBlock:
29
- block: ContentBlock = {"type": "text", "text": text}
28
+ def _set_meta(block: dict[str, Any], meta: dict[str, Any] | None) -> dict[str, Any]:
29
+ """Attach a copy of ``meta`` under the shared ``meta`` key, if non-empty."""
30
+
30
31
  if meta:
31
32
  block["meta"] = dict(meta)
32
33
  return block
33
34
 
34
35
 
36
+ def text_block(text: str, *, meta: dict[str, Any] | None = None) -> ContentBlock:
37
+ return _set_meta({"type": "text", "text": text}, meta)
38
+
39
+
35
40
  def thinking_block(text: str, *, meta: dict[str, Any] | None = None) -> ContentBlock:
36
- block: ContentBlock = {"type": "thinking", "text": text}
37
- if meta:
38
- block["meta"] = dict(meta)
39
- return block
41
+ return _set_meta({"type": "thinking", "text": text}, meta)
40
42
 
41
43
 
42
44
  def image_block(
@@ -49,9 +51,7 @@ def image_block(
49
51
  block: ContentBlock = {"type": "image", "data": data, "mime_type": mime_type}
50
52
  if name:
51
53
  block["name"] = name
52
- if meta:
53
- block["meta"] = dict(meta)
54
- return block
54
+ return _set_meta(block, meta)
55
55
 
56
56
 
57
57
  def document_block(
@@ -64,9 +64,7 @@ def document_block(
64
64
  block: ContentBlock = {"type": "document", "data": data, "mime_type": mime_type}
65
65
  if name:
66
66
  block["name"] = name
67
- if meta:
68
- block["meta"] = dict(meta)
69
- return block
67
+ return _set_meta(block, meta)
70
68
 
71
69
 
72
70
  def tool_use_block(
@@ -82,9 +80,7 @@ def tool_use_block(
82
80
  "name": name,
83
81
  "input": dict(input or {}),
84
82
  }
85
- if meta:
86
- block["meta"] = dict(meta)
87
- return block
83
+ return _set_meta(block, meta)
88
84
 
89
85
 
90
86
  def tool_result_block(
@@ -114,9 +110,7 @@ def tool_result_block(
114
110
  block["metadata"] = dict(metadata)
115
111
  if content:
116
112
  block["content"] = [dict(item) for item in content]
117
- if meta:
118
- block["meta"] = dict(meta)
119
- return block
113
+ return _set_meta(block, meta)
120
114
 
121
115
 
122
116
  def user_text_message(text: str, *, meta: dict[str, Any] | None = None) -> ConversationMessage:
@@ -129,10 +123,7 @@ def build_message(
129
123
  *,
130
124
  meta: dict[str, Any] | None = None,
131
125
  ) -> ConversationMessage:
132
- message: ConversationMessage = {"role": role, "content": blocks}
133
- if meta:
134
- message["meta"] = dict(meta)
135
- return message
126
+ return _set_meta({"role": role, "content": blocks}, meta)
136
127
 
137
128
 
138
129
  def assistant_message(
@@ -66,16 +66,26 @@ def resolve_model_metadata(
66
66
  *,
67
67
  provider: str,
68
68
  model: str,
69
- **overrides: Any,
69
+ context_window: int | None = None,
70
+ max_output_tokens: int | None = None,
71
+ supports_reasoning: bool | None = None,
72
+ supports_image_input: bool | None = None,
73
+ supports_pdf_input: bool | None = None,
70
74
  ) -> ModelMetadata:
71
75
  """Return catalog metadata for ``(provider, model)`` with non-None overrides layered on top.
72
76
 
73
- Override keys must match :class:`ModelMetadata` fields. Missing overrides
74
- and an absent catalog entry both leave the corresponding fields at ``None``
75
- so callers can apply their own fallback defaults.
77
+ Missing overrides and an absent catalog entry both leave the corresponding
78
+ fields at ``None`` so callers can apply their own fallback defaults.
76
79
  """
77
80
 
78
81
  base = lookup_model_metadata(provider_type=provider, model=model) or ModelMetadata(provider=provider, model=model)
82
+ overrides = {
83
+ "context_window": context_window,
84
+ "max_output_tokens": max_output_tokens,
85
+ "supports_reasoning": supports_reasoning,
86
+ "supports_image_input": supports_image_input,
87
+ "supports_pdf_input": supports_pdf_input,
88
+ }
79
89
  return replace(base, **{k: v for k, v in overrides.items() if v is not None})
80
90
 
81
91