mycode-sdk 0.10.0__tar.gz → 0.11.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.
Files changed (23) hide show
  1. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/PKG-INFO +5 -3
  2. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/README.md +4 -2
  3. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/pyproject.toml +1 -1
  4. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/agent.py +57 -8
  5. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/models_catalog.json +187 -129
  6. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/providers/__init__.py +9 -1
  7. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/providers/anthropic_like.py +8 -2
  8. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/providers/openai_chat.py +25 -2
  9. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/tools.py +85 -11
  10. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/.gitignore +0 -0
  11. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/LICENSE +0 -0
  12. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/__init__.py +0 -0
  13. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/attachments.py +0 -0
  14. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/compact.py +0 -0
  15. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/hooks.py +0 -0
  16. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/messages.py +0 -0
  17. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/models.py +0 -0
  18. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/providers/base.py +0 -0
  19. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/providers/gemini.py +0 -0
  20. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/providers/openai_responses.py +0 -0
  21. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/py.typed +0 -0
  22. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/session.py +0 -0
  23. {mycode_sdk-0.10.0 → mycode_sdk-0.11.1}/src/mycode/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycode-sdk
3
- Version: 0.10.0
3
+ Version: 0.11.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
@@ -71,7 +71,7 @@ print(result.text)
71
71
 
72
72
  ## Multi-turn conversations
73
73
 
74
- Call `achat()` or `run()` again on the same `Agent` — history accumulates automatically:
74
+ Call `achat()` or `run()` again on the same `Agent` to continue the conversation:
75
75
 
76
76
  ```python
77
77
  agent = Agent(model="claude-sonnet-4-6", api_key="...")
@@ -117,7 +117,7 @@ agent = Agent(
117
117
  )
118
118
  ```
119
119
 
120
- Construct another `Agent` with the same `(session_dir, session_id)` later to resume the conversation — the history is loaded automatically.
120
+ Construct another `Agent` with the same `(session_dir, session_id)` to load the conversation history.
121
121
 
122
122
  ## Built-in tools
123
123
 
@@ -167,6 +167,8 @@ def summarize_file(ctx: ToolContext, path: str) -> str:
167
167
  return result.output.splitlines()[0] if result.output else ""
168
168
  ```
169
169
 
170
+ Async tools use `await ctx.aread()`, `await ctx.awrite()`, `await ctx.aedit()`, and `await ctx.abash()`. Use `await ctx.acall(name, args)` to call another registered tool by name.
171
+
170
172
  ## Tool hooks
171
173
 
172
174
  Inspect or replace tool calls before they run. Return `None` from `before_tool` to let the tool execute, or a `ToolExecutionResult` to skip it:
@@ -44,7 +44,7 @@ print(result.text)
44
44
 
45
45
  ## Multi-turn conversations
46
46
 
47
- Call `achat()` or `run()` again on the same `Agent` — history accumulates automatically:
47
+ Call `achat()` or `run()` again on the same `Agent` to continue the conversation:
48
48
 
49
49
  ```python
50
50
  agent = Agent(model="claude-sonnet-4-6", api_key="...")
@@ -90,7 +90,7 @@ agent = Agent(
90
90
  )
91
91
  ```
92
92
 
93
- Construct another `Agent` with the same `(session_dir, session_id)` later to resume the conversation — the history is loaded automatically.
93
+ Construct another `Agent` with the same `(session_dir, session_id)` to load the conversation history.
94
94
 
95
95
  ## Built-in tools
96
96
 
@@ -140,6 +140,8 @@ def summarize_file(ctx: ToolContext, path: str) -> str:
140
140
  return result.output.splitlines()[0] if result.output else ""
141
141
  ```
142
142
 
143
+ Async tools use `await ctx.aread()`, `await ctx.awrite()`, `await ctx.aedit()`, and `await ctx.abash()`. Use `await ctx.acall(name, args)` to call another registered tool by name.
144
+
143
145
  ## Tool hooks
144
146
 
145
147
  Inspect or replace tool calls before they run. Return `None` from `before_tool` to let the tool execute, or a `ToolExecutionResult` to skip it:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mycode-sdk"
7
- version = "0.10.0"
7
+ version = "0.11.1"
8
8
  description = "Lightweight Python SDK for building AI agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -87,6 +87,7 @@ class Agent:
87
87
  compact_threshold: float | None = None,
88
88
  reasoning_effort: str | None = None,
89
89
  supports_reasoning: bool | None = None,
90
+ supports_reasoning_effort: bool = False,
90
91
  supports_image_input: bool | None = None,
91
92
  supports_pdf_input: bool | None = None,
92
93
  system: str = "",
@@ -127,11 +128,16 @@ class Agent:
127
128
  self.temperature = float(temperature)
128
129
  self.compact_threshold = compact_threshold if compact_threshold is not None else DEFAULT_COMPACT_THRESHOLD
129
130
  self.reasoning_effort = reasoning_effort
131
+ # Whether the endpoint accepts the effort knob. Comes from provider
132
+ # config, not the catalog, so it stays out of refresh_capabilities.
133
+ self.supports_reasoning_effort = supports_reasoning_effort
130
134
 
131
135
  self.system = system
132
136
  self.hooks = hooks or Hooks()
133
137
  self._cancel_event = asyncio.Event()
138
+ self._event_loop: asyncio.AbstractEventLoop | None = None
134
139
  self._provider_event_task: asyncio.Future[ProviderStreamEvent] | None = None
140
+ self._active_tool_task: asyncio.Task[ToolExecutionResult] | None = None
135
141
 
136
142
  # History resolution:
137
143
  # - messages is None → auto-resume from disk if the session exists
@@ -201,10 +207,24 @@ class Agent:
201
207
  def cancel(self) -> None:
202
208
  """Request cancellation of the in-flight turn."""
203
209
 
210
+ try:
211
+ running_loop = asyncio.get_running_loop()
212
+ except RuntimeError:
213
+ running_loop = None
214
+
215
+ loop = self._event_loop
216
+ if loop is not None and loop.is_running() and running_loop is not loop:
217
+ loop.call_soon_threadsafe(self._cancel_in_loop)
218
+ return
219
+ self._cancel_in_loop()
220
+
221
+ def _cancel_in_loop(self) -> None:
204
222
  self._cancel_event.set()
205
223
  self.tools.cancel_active()
206
224
  if self._provider_event_task and not self._provider_event_task.done():
207
225
  self._provider_event_task.cancel()
226
+ if self._active_tool_task and not self._active_tool_task.done():
227
+ self._active_tool_task.cancel()
208
228
 
209
229
  def clear(self) -> None:
210
230
  """Drop the in-memory conversation history."""
@@ -215,6 +235,23 @@ class Agent:
215
235
  # Tool execution
216
236
  # ------------------------------------------------------------------
217
237
 
238
+ async def _execute_tool(
239
+ self,
240
+ spec: ToolSpec,
241
+ args: dict[str, Any],
242
+ ctx: ToolContext,
243
+ ) -> ToolExecutionResult:
244
+ if not spec.is_async:
245
+ return await self.tools.aexecute(spec.name, args, ctx)
246
+
247
+ task = asyncio.create_task(self.tools.aexecute(spec.name, args, ctx))
248
+ self._active_tool_task = task
249
+ try:
250
+ return await task
251
+ finally:
252
+ if self._active_tool_task is task:
253
+ self._active_tool_task = None
254
+
218
255
  async def _run_tool_call(self, tool_use: dict[str, Any]) -> AsyncIterator[Event]:
219
256
  """Run one tool call and emit the standard tool events."""
220
257
 
@@ -261,13 +298,23 @@ class Agent:
261
298
  return
262
299
 
263
300
  if spec.streams_output:
264
- async for event in self._run_streaming_tool(tool_id=tool_id, name=name, args=args, hook_ctx=hook_ctx):
301
+ async for event in self._run_streaming_tool(
302
+ tool_id=tool_id,
303
+ spec=spec,
304
+ args=args,
305
+ hook_ctx=hook_ctx,
306
+ ):
265
307
  yield event
266
308
  return
267
309
 
268
310
  try:
269
311
  ctx = self._ctx_for_call(tool_id)
270
- result = await asyncio.to_thread(self.tools.execute, name, args, ctx)
312
+ result = await self._execute_tool(spec, args, ctx)
313
+ except asyncio.CancelledError:
314
+ if self._cancel_event.is_set():
315
+ yield self._error_done(tool_id, "error: cancelled")
316
+ return
317
+ raise
271
318
  except Exception as exc: # pragma: no cover - defensive
272
319
  result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
273
320
 
@@ -277,7 +324,7 @@ class Agent:
277
324
  self,
278
325
  *,
279
326
  tool_id: str,
280
- name: str,
327
+ spec: ToolSpec,
281
328
  args: dict[str, Any],
282
329
  hook_ctx: ToolHookContext,
283
330
  ) -> AsyncIterator[Event]:
@@ -291,13 +338,13 @@ class Agent:
291
338
 
292
339
  ctx = self._ctx_for_call(tool_id, emit=on_output)
293
340
 
294
- async def run_in_thread() -> ToolExecutionResult:
341
+ async def run_tool() -> ToolExecutionResult:
295
342
  try:
296
- return await asyncio.to_thread(self.tools.execute, name, args, ctx)
343
+ return await self._execute_tool(spec, args, ctx)
297
344
  finally:
298
345
  loop.call_soon_threadsafe(output_queue.put_nowait, None)
299
346
 
300
- task = asyncio.create_task(run_in_thread())
347
+ task = asyncio.create_task(run_tool())
301
348
  was_cancelled = False
302
349
  output_parts: list[str] = []
303
350
 
@@ -319,8 +366,8 @@ class Agent:
319
366
  output_parts.append(output)
320
367
  yield Event("tool_output", {"tool_use_id": tool_id, "output": output})
321
368
 
322
- if was_cancelled:
323
- with suppress(Exception):
369
+ if was_cancelled or (self._cancel_event.is_set() and task.cancelled()):
370
+ with suppress(asyncio.CancelledError, Exception):
324
371
  await task
325
372
  output = "\n".join([*output_parts, "error: cancelled"]) if output_parts else "error: cancelled"
326
373
  yield self._error_done(tool_id, output)
@@ -490,6 +537,7 @@ class Agent:
490
537
  async def persist(message: ConversationMessage) -> None:
491
538
  await self._persist_message(message, on_persist)
492
539
 
540
+ self._event_loop = asyncio.get_running_loop()
493
541
  self._cancel_event.clear()
494
542
 
495
543
  user_message: ConversationMessage
@@ -742,6 +790,7 @@ class Agent:
742
790
  :meth:`cancel` stops the summary request.
743
791
  """
744
792
 
793
+ self._event_loop = asyncio.get_running_loop()
745
794
  self._cancel_event.clear()
746
795
  adapter = get_provider_adapter(self.provider)
747
796
  return await self._compact(adapter, on_persist)