mycode-sdk 0.10.0__tar.gz → 0.11.0__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.10.0 → mycode_sdk-0.11.0}/PKG-INFO +5 -3
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/README.md +4 -2
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/pyproject.toml +1 -1
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/agent.py +53 -8
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/tools.py +85 -11
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/.gitignore +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/LICENSE +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/__init__.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/attachments.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/compact.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/hooks.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/messages.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/models.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/models_catalog.json +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/providers/anthropic_like.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/providers/base.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/providers/gemini.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/providers/openai_chat.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/providers/openai_responses.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/py.typed +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/session.py +0 -0
- {mycode_sdk-0.10.0 → mycode_sdk-0.11.0}/src/mycode/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycode-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
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`
|
|
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)`
|
|
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`
|
|
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)`
|
|
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:
|
|
@@ -131,7 +131,9 @@ class Agent:
|
|
|
131
131
|
self.system = system
|
|
132
132
|
self.hooks = hooks or Hooks()
|
|
133
133
|
self._cancel_event = asyncio.Event()
|
|
134
|
+
self._event_loop: asyncio.AbstractEventLoop | None = None
|
|
134
135
|
self._provider_event_task: asyncio.Future[ProviderStreamEvent] | None = None
|
|
136
|
+
self._active_tool_task: asyncio.Task[ToolExecutionResult] | None = None
|
|
135
137
|
|
|
136
138
|
# History resolution:
|
|
137
139
|
# - messages is None → auto-resume from disk if the session exists
|
|
@@ -201,10 +203,24 @@ class Agent:
|
|
|
201
203
|
def cancel(self) -> None:
|
|
202
204
|
"""Request cancellation of the in-flight turn."""
|
|
203
205
|
|
|
206
|
+
try:
|
|
207
|
+
running_loop = asyncio.get_running_loop()
|
|
208
|
+
except RuntimeError:
|
|
209
|
+
running_loop = None
|
|
210
|
+
|
|
211
|
+
loop = self._event_loop
|
|
212
|
+
if loop is not None and loop.is_running() and running_loop is not loop:
|
|
213
|
+
loop.call_soon_threadsafe(self._cancel_in_loop)
|
|
214
|
+
return
|
|
215
|
+
self._cancel_in_loop()
|
|
216
|
+
|
|
217
|
+
def _cancel_in_loop(self) -> None:
|
|
204
218
|
self._cancel_event.set()
|
|
205
219
|
self.tools.cancel_active()
|
|
206
220
|
if self._provider_event_task and not self._provider_event_task.done():
|
|
207
221
|
self._provider_event_task.cancel()
|
|
222
|
+
if self._active_tool_task and not self._active_tool_task.done():
|
|
223
|
+
self._active_tool_task.cancel()
|
|
208
224
|
|
|
209
225
|
def clear(self) -> None:
|
|
210
226
|
"""Drop the in-memory conversation history."""
|
|
@@ -215,6 +231,23 @@ class Agent:
|
|
|
215
231
|
# Tool execution
|
|
216
232
|
# ------------------------------------------------------------------
|
|
217
233
|
|
|
234
|
+
async def _execute_tool(
|
|
235
|
+
self,
|
|
236
|
+
spec: ToolSpec,
|
|
237
|
+
args: dict[str, Any],
|
|
238
|
+
ctx: ToolContext,
|
|
239
|
+
) -> ToolExecutionResult:
|
|
240
|
+
if not spec.is_async:
|
|
241
|
+
return await self.tools.aexecute(spec.name, args, ctx)
|
|
242
|
+
|
|
243
|
+
task = asyncio.create_task(self.tools.aexecute(spec.name, args, ctx))
|
|
244
|
+
self._active_tool_task = task
|
|
245
|
+
try:
|
|
246
|
+
return await task
|
|
247
|
+
finally:
|
|
248
|
+
if self._active_tool_task is task:
|
|
249
|
+
self._active_tool_task = None
|
|
250
|
+
|
|
218
251
|
async def _run_tool_call(self, tool_use: dict[str, Any]) -> AsyncIterator[Event]:
|
|
219
252
|
"""Run one tool call and emit the standard tool events."""
|
|
220
253
|
|
|
@@ -261,13 +294,23 @@ class Agent:
|
|
|
261
294
|
return
|
|
262
295
|
|
|
263
296
|
if spec.streams_output:
|
|
264
|
-
async for event in self._run_streaming_tool(
|
|
297
|
+
async for event in self._run_streaming_tool(
|
|
298
|
+
tool_id=tool_id,
|
|
299
|
+
spec=spec,
|
|
300
|
+
args=args,
|
|
301
|
+
hook_ctx=hook_ctx,
|
|
302
|
+
):
|
|
265
303
|
yield event
|
|
266
304
|
return
|
|
267
305
|
|
|
268
306
|
try:
|
|
269
307
|
ctx = self._ctx_for_call(tool_id)
|
|
270
|
-
result = await
|
|
308
|
+
result = await self._execute_tool(spec, args, ctx)
|
|
309
|
+
except asyncio.CancelledError:
|
|
310
|
+
if self._cancel_event.is_set():
|
|
311
|
+
yield self._error_done(tool_id, "error: cancelled")
|
|
312
|
+
return
|
|
313
|
+
raise
|
|
271
314
|
except Exception as exc: # pragma: no cover - defensive
|
|
272
315
|
result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
|
|
273
316
|
|
|
@@ -277,7 +320,7 @@ class Agent:
|
|
|
277
320
|
self,
|
|
278
321
|
*,
|
|
279
322
|
tool_id: str,
|
|
280
|
-
|
|
323
|
+
spec: ToolSpec,
|
|
281
324
|
args: dict[str, Any],
|
|
282
325
|
hook_ctx: ToolHookContext,
|
|
283
326
|
) -> AsyncIterator[Event]:
|
|
@@ -291,13 +334,13 @@ class Agent:
|
|
|
291
334
|
|
|
292
335
|
ctx = self._ctx_for_call(tool_id, emit=on_output)
|
|
293
336
|
|
|
294
|
-
async def
|
|
337
|
+
async def run_tool() -> ToolExecutionResult:
|
|
295
338
|
try:
|
|
296
|
-
return await
|
|
339
|
+
return await self._execute_tool(spec, args, ctx)
|
|
297
340
|
finally:
|
|
298
341
|
loop.call_soon_threadsafe(output_queue.put_nowait, None)
|
|
299
342
|
|
|
300
|
-
task = asyncio.create_task(
|
|
343
|
+
task = asyncio.create_task(run_tool())
|
|
301
344
|
was_cancelled = False
|
|
302
345
|
output_parts: list[str] = []
|
|
303
346
|
|
|
@@ -319,8 +362,8 @@ class Agent:
|
|
|
319
362
|
output_parts.append(output)
|
|
320
363
|
yield Event("tool_output", {"tool_use_id": tool_id, "output": output})
|
|
321
364
|
|
|
322
|
-
if was_cancelled:
|
|
323
|
-
with suppress(Exception):
|
|
365
|
+
if was_cancelled or (self._cancel_event.is_set() and task.cancelled()):
|
|
366
|
+
with suppress(asyncio.CancelledError, Exception):
|
|
324
367
|
await task
|
|
325
368
|
output = "\n".join([*output_parts, "error: cancelled"]) if output_parts else "error: cancelled"
|
|
326
369
|
yield self._error_done(tool_id, output)
|
|
@@ -490,6 +533,7 @@ class Agent:
|
|
|
490
533
|
async def persist(message: ConversationMessage) -> None:
|
|
491
534
|
await self._persist_message(message, on_persist)
|
|
492
535
|
|
|
536
|
+
self._event_loop = asyncio.get_running_loop()
|
|
493
537
|
self._cancel_event.clear()
|
|
494
538
|
|
|
495
539
|
user_message: ConversationMessage
|
|
@@ -742,6 +786,7 @@ class Agent:
|
|
|
742
786
|
:meth:`cancel` stops the summary request.
|
|
743
787
|
"""
|
|
744
788
|
|
|
789
|
+
self._event_loop = asyncio.get_running_loop()
|
|
745
790
|
self._cancel_event.clear()
|
|
746
791
|
adapter = get_provider_adapter(self.provider)
|
|
747
792
|
return await self._compact(adapter, on_persist)
|
|
@@ -13,6 +13,7 @@ facades on :class:`ToolContext`::
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
|
+
import functools
|
|
16
17
|
import inspect
|
|
17
18
|
import json
|
|
18
19
|
import os
|
|
@@ -25,7 +26,7 @@ import time
|
|
|
25
26
|
import typing
|
|
26
27
|
from base64 import b64encode
|
|
27
28
|
from collections import deque
|
|
28
|
-
from collections.abc import Callable, Mapping, Sequence
|
|
29
|
+
from collections.abc import Callable, Coroutine, Mapping, Sequence
|
|
29
30
|
from contextlib import suppress
|
|
30
31
|
from dataclasses import dataclass
|
|
31
32
|
from difflib import SequenceMatcher, unified_diff
|
|
@@ -71,7 +72,19 @@ class ToolExecutionResult:
|
|
|
71
72
|
is_error: bool = False
|
|
72
73
|
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
SyncToolRunner = Callable[["ToolContext", dict[str, Any]], ToolExecutionResult]
|
|
76
|
+
AsyncToolRunner = Callable[["ToolContext", dict[str, Any]], Coroutine[Any, Any, ToolExecutionResult]]
|
|
77
|
+
ToolRunner = SyncToolRunner | AsyncToolRunner
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_async_callable(runner: ToolRunner) -> bool:
|
|
81
|
+
current: Any = runner
|
|
82
|
+
while True:
|
|
83
|
+
current = inspect.unwrap(current)
|
|
84
|
+
if isinstance(current, functools.partial):
|
|
85
|
+
current = current.func
|
|
86
|
+
continue
|
|
87
|
+
return inspect.iscoroutinefunction(current) or inspect.iscoroutinefunction(type(current).__call__)
|
|
75
88
|
|
|
76
89
|
|
|
77
90
|
@dataclass(frozen=True)
|
|
@@ -85,6 +98,10 @@ class ToolSpec:
|
|
|
85
98
|
# Streaming tools push incremental output through ToolContext.emit.
|
|
86
99
|
streams_output: bool = False
|
|
87
100
|
|
|
101
|
+
@property
|
|
102
|
+
def is_async(self) -> bool:
|
|
103
|
+
return _is_async_callable(self.runner)
|
|
104
|
+
|
|
88
105
|
|
|
89
106
|
# ---------------------------------------------------------------------------
|
|
90
107
|
# ToolContext
|
|
@@ -115,20 +132,48 @@ class ToolContext:
|
|
|
115
132
|
) -> ToolExecutionResult:
|
|
116
133
|
return self.call("read", {"path": path, "offset": offset, "limit": limit})
|
|
117
134
|
|
|
135
|
+
async def aread(
|
|
136
|
+
self,
|
|
137
|
+
path: str,
|
|
138
|
+
*,
|
|
139
|
+
offset: int | None = None,
|
|
140
|
+
limit: int | None = None,
|
|
141
|
+
) -> ToolExecutionResult:
|
|
142
|
+
return await self.acall("read", {"path": path, "offset": offset, "limit": limit})
|
|
143
|
+
|
|
118
144
|
def write(self, path: str, content: str) -> ToolExecutionResult:
|
|
119
145
|
return self.call("write", {"path": path, "content": content})
|
|
120
146
|
|
|
147
|
+
async def awrite(self, path: str, content: str) -> ToolExecutionResult:
|
|
148
|
+
return await self.acall("write", {"path": path, "content": content})
|
|
149
|
+
|
|
121
150
|
def edit(self, path: str, edits: list[dict[str, str]]) -> ToolExecutionResult:
|
|
122
151
|
return self.call("edit", {"path": path, "edits": edits})
|
|
123
152
|
|
|
153
|
+
async def aedit(self, path: str, edits: list[dict[str, str]]) -> ToolExecutionResult:
|
|
154
|
+
return await self.acall("edit", {"path": path, "edits": edits})
|
|
155
|
+
|
|
124
156
|
def bash(self, command: str, *, timeout: int | None = None) -> ToolExecutionResult:
|
|
125
157
|
return self.call("bash", {"command": command, "timeout": timeout})
|
|
126
158
|
|
|
159
|
+
async def abash(
|
|
160
|
+
self,
|
|
161
|
+
command: str,
|
|
162
|
+
*,
|
|
163
|
+
timeout: int | None = None, # noqa: ASYNC109
|
|
164
|
+
) -> ToolExecutionResult:
|
|
165
|
+
return await self.acall("bash", {"command": command, "timeout": timeout})
|
|
166
|
+
|
|
127
167
|
def call(self, name: str, args: dict[str, Any]) -> ToolExecutionResult:
|
|
128
168
|
"""Dispatch through the registry, including from custom tool wrappers."""
|
|
129
169
|
|
|
130
170
|
return self.executor.execute(name, args, self)
|
|
131
171
|
|
|
172
|
+
async def acall(self, name: str, args: dict[str, Any]) -> ToolExecutionResult:
|
|
173
|
+
"""Asynchronously dispatch through the registry."""
|
|
174
|
+
|
|
175
|
+
return await self.executor.aexecute(name, args, self)
|
|
176
|
+
|
|
132
177
|
|
|
133
178
|
# ---------------------------------------------------------------------------
|
|
134
179
|
# ToolExecutor
|
|
@@ -160,7 +205,21 @@ class ToolExecutor:
|
|
|
160
205
|
spec = self._tools.get(name)
|
|
161
206
|
if spec is None:
|
|
162
207
|
return ToolExecutionResult(output=f"error: unknown tool: {name}", is_error=True)
|
|
163
|
-
|
|
208
|
+
if spec.is_async:
|
|
209
|
+
runner = cast(AsyncToolRunner, spec.runner)
|
|
210
|
+
return asyncio.run(runner(ctx, args))
|
|
211
|
+
runner = cast(SyncToolRunner, spec.runner)
|
|
212
|
+
return runner(ctx, args)
|
|
213
|
+
|
|
214
|
+
async def aexecute(self, name: str, args: dict[str, Any], ctx: ToolContext) -> ToolExecutionResult:
|
|
215
|
+
spec = self._tools.get(name)
|
|
216
|
+
if spec is None:
|
|
217
|
+
return ToolExecutionResult(output=f"error: unknown tool: {name}", is_error=True)
|
|
218
|
+
if spec.is_async:
|
|
219
|
+
runner = cast(AsyncToolRunner, spec.runner)
|
|
220
|
+
return await runner(ctx, args)
|
|
221
|
+
runner = cast(SyncToolRunner, spec.runner)
|
|
222
|
+
return await asyncio.to_thread(runner, ctx, args)
|
|
164
223
|
|
|
165
224
|
# Subprocess tracking: register with both this executor (per-session
|
|
166
225
|
# cancel) and the process-global set (shutdown cleanup).
|
|
@@ -319,7 +378,7 @@ def tool(
|
|
|
319
378
|
|
|
320
379
|
is_async = inspect.iscoroutinefunction(fn)
|
|
321
380
|
|
|
322
|
-
def
|
|
381
|
+
def prepare_args(args: dict[str, Any]) -> dict[str, Any] | ToolExecutionResult:
|
|
323
382
|
# Strict providers send explicit null for an omitted optional field;
|
|
324
383
|
# drop it so the parameter default applies instead of failing validation.
|
|
325
384
|
validation_args = {
|
|
@@ -332,14 +391,29 @@ def tool(
|
|
|
332
391
|
except ValidationError as exc:
|
|
333
392
|
return ToolExecutionResult(output=f"error: invalid tool input: {exc}", is_error=True)
|
|
334
393
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
394
|
+
return {name: getattr(parsed_args, name) for name in tool_param_names}
|
|
395
|
+
|
|
396
|
+
if is_async:
|
|
397
|
+
|
|
398
|
+
async def async_runner(context: ToolContext, args: dict[str, Any]) -> ToolExecutionResult:
|
|
399
|
+
call_args = prepare_args(args)
|
|
400
|
+
if isinstance(call_args, ToolExecutionResult):
|
|
401
|
+
return call_args
|
|
402
|
+
value = await (fn(context, **call_args) if wants_context else fn(**call_args))
|
|
403
|
+
return _coerce_tool_result(value)
|
|
404
|
+
|
|
405
|
+
runner: ToolRunner = async_runner
|
|
406
|
+
|
|
407
|
+
else:
|
|
408
|
+
|
|
409
|
+
def sync_runner(context: ToolContext, args: dict[str, Any]) -> ToolExecutionResult:
|
|
410
|
+
call_args = prepare_args(args)
|
|
411
|
+
if isinstance(call_args, ToolExecutionResult):
|
|
412
|
+
return call_args
|
|
341
413
|
value = fn(context, **call_args) if wants_context else fn(**call_args)
|
|
342
|
-
|
|
414
|
+
return _coerce_tool_result(value)
|
|
415
|
+
|
|
416
|
+
runner = sync_runner
|
|
343
417
|
|
|
344
418
|
return ToolSpec(
|
|
345
419
|
name=tool_name,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|