mycode-sdk 0.8.7__tar.gz → 0.8.8__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.8.7 → mycode_sdk-0.8.8}/PKG-INFO +4 -4
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/pyproject.toml +4 -4
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/__init__.py +0 -3
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/agent.py +8 -20
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/messages.py +13 -18
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/models.py +1 -6
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/providers/anthropic_like.py +10 -17
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/providers/base.py +7 -9
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/providers/gemini.py +2 -1
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/providers/openai_chat.py +7 -6
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/providers/openai_responses.py +2 -1
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/session.py +6 -10
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/tools.py +27 -67
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/.gitignore +0 -0
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/LICENSE +0 -0
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/README.md +0 -0
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/compact.py +0 -0
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/hooks.py +0 -0
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/models_catalog.json +0 -0
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/py.typed +0 -0
- {mycode_sdk-0.8.7 → mycode_sdk-0.8.8}/src/mycode/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycode-sdk
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.8
|
|
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,9 +18,9 @@ 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.
|
|
22
|
-
Requires-Dist: google-genai>=
|
|
23
|
-
Requires-Dist: openai>=2.
|
|
21
|
+
Requires-Dist: anthropic>=0.102.0
|
|
22
|
+
Requires-Dist: google-genai>=2.3.0
|
|
23
|
+
Requires-Dist: openai>=2.36.0
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
26
26
|
# mycode-sdk
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mycode-sdk"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.8"
|
|
8
8
|
description = "Lightweight Python SDK for building AI agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -23,9 +23,9 @@ classifiers = [
|
|
|
23
23
|
]
|
|
24
24
|
keywords = ["agent", "llm", "anthropic", "openai", "gemini", "sdk"]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"anthropic>=0.
|
|
27
|
-
"google-genai>=
|
|
28
|
-
"openai>=2.
|
|
26
|
+
"anthropic>=0.102.0",
|
|
27
|
+
"google-genai>=2.3.0",
|
|
28
|
+
"openai>=2.36.0",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[project.urls]
|
|
@@ -39,9 +39,6 @@ from mycode.tools import (
|
|
|
39
39
|
# The package metadata in mycode/pyproject.toml is the single version source.
|
|
40
40
|
__version__ = metadata.version("mycode-sdk")
|
|
41
41
|
|
|
42
|
-
# Built-in tool specs exposed as module-level constants so callers can pick
|
|
43
|
-
# which ones to register (``tools=[read_tool, bash_tool]``) rather than
|
|
44
|
-
# getting all four by default.
|
|
45
42
|
read_tool, write_tool, edit_tool, bash_tool = DEFAULT_TOOL_SPECS
|
|
46
43
|
|
|
47
44
|
__all__ = [
|
|
@@ -136,10 +136,9 @@ class Agent:
|
|
|
136
136
|
raise ValueError(msg)
|
|
137
137
|
self.messages: list[ConversationMessage] = list(messages)
|
|
138
138
|
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
# When no session is configured, use a tempdir scoped to session_id.
|
|
139
|
+
# ``tool_output_dir`` defaults to a session-adjacent directory so logs
|
|
140
|
+
# (e.g. bash spill files) live next to the session JSONL; without a
|
|
141
|
+
# session, fall back to a tempdir scoped to ``session_id``.
|
|
143
142
|
if session_dir is not None:
|
|
144
143
|
tool_output_dir = session_dir / self.session_id / "tool-output"
|
|
145
144
|
else:
|
|
@@ -434,21 +433,12 @@ class Agent:
|
|
|
434
433
|
*,
|
|
435
434
|
on_persist: PersistCallback | None = None,
|
|
436
435
|
) -> AsyncIterator[Event]:
|
|
437
|
-
"""Run the full agent loop for one user message.
|
|
438
|
-
|
|
439
|
-
Each turn asks the provider for one assistant message. If the assistant
|
|
440
|
-
requests tools, the agent runs them locally, appends one user-side
|
|
441
|
-
tool_result message, and continues until the assistant stops using tools.
|
|
442
|
-
|
|
443
|
-
When a ``session_dir`` is configured, every emitted message is appended
|
|
444
|
-
to the on-disk session log. ``on_persist`` fires *before* that append
|
|
445
|
-
regardless of whether a store is configured, so callers can plug in a
|
|
446
|
-
custom backend or stage related records (the web server uses it to
|
|
447
|
-
land a rewind marker first).
|
|
448
|
-
"""
|
|
436
|
+
"""Run the full agent loop for one user message."""
|
|
449
437
|
|
|
450
438
|
async def persist(message: ConversationMessage) -> None:
|
|
451
439
|
if on_persist is not None:
|
|
440
|
+
# Callers may need to write related records before the SDK
|
|
441
|
+
# appends this message to its own session log.
|
|
452
442
|
await on_persist(message)
|
|
453
443
|
if self._store is None:
|
|
454
444
|
return
|
|
@@ -517,7 +507,6 @@ class Agent:
|
|
|
517
507
|
)
|
|
518
508
|
|
|
519
509
|
try:
|
|
520
|
-
# Phase 1: ask the provider for exactly one assistant turn.
|
|
521
510
|
async for provider_event in self._stream_provider_turn(adapter, request):
|
|
522
511
|
if self._cancel_event.is_set():
|
|
523
512
|
provider_cancelled = True
|
|
@@ -675,8 +664,8 @@ class Agent:
|
|
|
675
664
|
yield Event("error", {"message": "cancelled"})
|
|
676
665
|
return
|
|
677
666
|
except Exception:
|
|
678
|
-
#
|
|
679
|
-
#
|
|
667
|
+
# Compaction must not block the current answer; the full
|
|
668
|
+
# transcript is still available for the next turn.
|
|
680
669
|
logger.warning(
|
|
681
670
|
"Context compaction failed, continuing without compaction",
|
|
682
671
|
exc_info=True,
|
|
@@ -686,7 +675,6 @@ class Agent:
|
|
|
686
675
|
break
|
|
687
676
|
|
|
688
677
|
else:
|
|
689
|
-
# while loop exhausted max_turns without breaking
|
|
690
678
|
yield Event("error", {"message": "max_turns reached"})
|
|
691
679
|
return
|
|
692
680
|
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
"""Internal conversation model shared by the runtime, session store, CLI, and UI.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
`context_window` (see docs/sessions.md for `total_tokens` semantics)
|
|
17
|
-
- provider-specific assistant message extras live under `meta.native`
|
|
18
|
-
- provider-specific block replay hints live under `block.meta.native`
|
|
19
|
-
- local display metadata, such as `block.meta.duration_ms`, is never sent
|
|
20
|
-
upstream; provider adapters must explicitly project only supported fields
|
|
3
|
+
Provider adapters translate this shape to and from provider-specific wire
|
|
4
|
+
formats so the agent loop and session store stay provider-agnostic.
|
|
5
|
+
|
|
6
|
+
Metadata layout:
|
|
7
|
+
|
|
8
|
+
- assistant message ``meta`` keeps only normalized top-level fields:
|
|
9
|
+
``provider``, ``model``, ``provider_message_id``, ``stop_reason``,
|
|
10
|
+
``total_tokens``, ``context_window`` (see docs/sessions.md for
|
|
11
|
+
``total_tokens`` semantics)
|
|
12
|
+
- provider-specific extras live under ``meta.native`` on messages and
|
|
13
|
+
``block.meta.native`` on blocks
|
|
14
|
+
- local display metadata such as ``block.meta.duration_ms`` is never sent
|
|
15
|
+
upstream
|
|
21
16
|
"""
|
|
22
17
|
|
|
23
18
|
from __future__ import annotations
|
|
@@ -42,12 +42,7 @@ def load_models_catalog() -> dict[str, Any] | None:
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def infer_provider_from_model(model: str | None) -> str | None:
|
|
45
|
-
"""Return the canonical built-in provider id for a known model id, else None.
|
|
46
|
-
|
|
47
|
-
Recognizes well-known prefixes on bare model ids and ``provider/model``
|
|
48
|
-
ids alike. Returns ``None`` for unknown ids — callers should require an
|
|
49
|
-
explicit provider in that case rather than guess.
|
|
50
|
-
"""
|
|
45
|
+
"""Return the canonical built-in provider id for a known model id, else None."""
|
|
51
46
|
|
|
52
47
|
bare = (model or "").strip().split("/", 1)[-1].strip().lower()
|
|
53
48
|
if bare.startswith("claude-"):
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
6
|
from collections.abc import AsyncIterator
|
|
7
|
-
from typing import Any, cast
|
|
7
|
+
from typing import Any, cast, override
|
|
8
8
|
|
|
9
9
|
from anthropic import APIError, AsyncAnthropic
|
|
10
10
|
|
|
@@ -32,19 +32,7 @@ _THINKING_BUDGETS: dict[str, int] = {"low": 2048, "medium": 8192, "high": 24576,
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class AnthropicLikeAdapter(ProviderAdapter):
|
|
35
|
-
"""Shared Messages adapter for Anthropic-compatible providers.
|
|
36
|
-
|
|
37
|
-
Anthropic, Moonshot, and MiniMax all document agent usage around the
|
|
38
|
-
Anthropic Messages protocol. The differences we care about are limited to:
|
|
39
|
-
|
|
40
|
-
- default base URL
|
|
41
|
-
- API key env var names
|
|
42
|
-
- optional thinking defaults
|
|
43
|
-
- provider-native metadata carried in content blocks
|
|
44
|
-
|
|
45
|
-
MiniMax requires the full assistant content (all blocks) to be sent on
|
|
46
|
-
multi-turn tool-loop requests — not just the text portion.
|
|
47
|
-
"""
|
|
35
|
+
"""Shared Messages adapter for Anthropic-compatible providers."""
|
|
48
36
|
|
|
49
37
|
def thinking_config(self, request: ProviderRequest) -> dict[str, Any] | None:
|
|
50
38
|
del request
|
|
@@ -90,6 +78,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
90
78
|
payload["output_config"] = output_config
|
|
91
79
|
return payload
|
|
92
80
|
|
|
81
|
+
@override
|
|
93
82
|
def project_tool_call_id(self, tool_call_id: str, used_tool_call_ids: set[str]) -> str:
|
|
94
83
|
"""Return a short ASCII ID without introducing collisions.
|
|
95
84
|
|
|
@@ -138,6 +127,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
138
127
|
|
|
139
128
|
return
|
|
140
129
|
|
|
130
|
+
@override
|
|
141
131
|
async def stream_turn(self, request: ProviderRequest) -> AsyncIterator[ProviderStreamEvent]:
|
|
142
132
|
api_key = self.require_api_key(request.api_key)
|
|
143
133
|
client = AsyncAnthropic(
|
|
@@ -326,6 +316,7 @@ class AnthropicAdapter(AnthropicLikeAdapter):
|
|
|
326
316
|
default_models = ("claude-sonnet-4-6", "claude-opus-4-7")
|
|
327
317
|
supports_reasoning_effort = True
|
|
328
318
|
|
|
319
|
+
@override
|
|
329
320
|
def thinking_config(self, request: ProviderRequest) -> dict[str, Any] | None:
|
|
330
321
|
effort = request.reasoning_effort
|
|
331
322
|
if not effort:
|
|
@@ -340,6 +331,7 @@ class AnthropicAdapter(AnthropicLikeAdapter):
|
|
|
340
331
|
return thinking
|
|
341
332
|
return self.manual_thinking_config(effort)
|
|
342
333
|
|
|
334
|
+
@override
|
|
343
335
|
def output_config(self, request: ProviderRequest) -> dict[str, Any] | None:
|
|
344
336
|
effort = request.reasoning_effort
|
|
345
337
|
if not effort or effort == "none":
|
|
@@ -363,9 +355,8 @@ class AnthropicAdapter(AnthropicLikeAdapter):
|
|
|
363
355
|
class MoonshotAIAdapter(AnthropicLikeAdapter):
|
|
364
356
|
"""Moonshot's Anthropic-compatible Messages endpoint.
|
|
365
357
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
Moonshot does not strip them on the server side.
|
|
358
|
+
When thinking is enabled, prior reasoning blocks must be replayed in the
|
|
359
|
+
conversation history — Moonshot does not strip them on the server side.
|
|
369
360
|
"""
|
|
370
361
|
|
|
371
362
|
provider_id = "moonshotai"
|
|
@@ -375,6 +366,7 @@ class MoonshotAIAdapter(AnthropicLikeAdapter):
|
|
|
375
366
|
default_models = ("kimi-k2.6",)
|
|
376
367
|
supports_reasoning_effort = True
|
|
377
368
|
|
|
369
|
+
@override
|
|
378
370
|
def thinking_config(self, request: ProviderRequest) -> dict[str, Any] | None:
|
|
379
371
|
return self.manual_thinking_config(request.reasoning_effort)
|
|
380
372
|
|
|
@@ -394,5 +386,6 @@ class MiniMaxAdapter(AnthropicLikeAdapter):
|
|
|
394
386
|
default_models = ("MiniMax-M2.7", "MiniMax-M2.7-highspeed")
|
|
395
387
|
supports_reasoning_effort = True
|
|
396
388
|
|
|
389
|
+
@override
|
|
397
390
|
def thinking_config(self, request: ProviderRequest) -> dict[str, Any] | None:
|
|
398
391
|
return self.manual_thinking_config(request.reasoning_effort)
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
"""Shared provider adapter interfaces.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
- output: streamed `ProviderStreamEvent` objects
|
|
7
|
-
|
|
8
|
-
Concrete adapters are free to use the official SDK or protocol that best matches
|
|
9
|
-
their upstream provider. Each adapter is also responsible for projecting the
|
|
10
|
-
canonical session transcript into a provider-safe replay history before a new
|
|
11
|
-
request is sent upstream.
|
|
3
|
+
Each adapter implements `stream_turn()` and reuses `prepare_messages()` to
|
|
4
|
+
project the canonical session transcript into a provider-safe replay history
|
|
5
|
+
before sending a request upstream.
|
|
12
6
|
"""
|
|
13
7
|
|
|
14
8
|
from __future__ import annotations
|
|
@@ -205,6 +199,8 @@ def repair_messages_for_replay(
|
|
|
205
199
|
block_type = raw_block.get("type")
|
|
206
200
|
if block_type in {"text", "thinking"}:
|
|
207
201
|
text = str(raw_block.get("text") or "")
|
|
202
|
+
# Empty native thinking blocks may still carry signatures or
|
|
203
|
+
# provider replay state.
|
|
208
204
|
if text or get_native_meta(raw_block):
|
|
209
205
|
content.append(dict(raw_block))
|
|
210
206
|
continue
|
|
@@ -256,6 +252,8 @@ def repair_messages_for_replay(
|
|
|
256
252
|
if supported:
|
|
257
253
|
content.append(dict(raw_block))
|
|
258
254
|
else:
|
|
255
|
+
# Preserve that the file existed even when the target
|
|
256
|
+
# provider cannot accept the original media payload.
|
|
259
257
|
default_mime = "image" if block_type == "image" else "application/pdf"
|
|
260
258
|
label = "image input" if block_type == "image" else "PDF input"
|
|
261
259
|
name = html.escape(str(raw_block.get("name") or f"attached-{block_type}"), quote=True)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import AsyncIterator
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any, override
|
|
7
7
|
from urllib.parse import urlparse
|
|
8
8
|
|
|
9
9
|
from google import genai
|
|
@@ -51,6 +51,7 @@ class GoogleGeminiAdapter(ProviderAdapter):
|
|
|
51
51
|
default_models = ("gemini-3.1-pro-preview", "gemini-3-flash-preview")
|
|
52
52
|
supports_reasoning_effort = True
|
|
53
53
|
|
|
54
|
+
@override
|
|
54
55
|
async def stream_turn(self, request: ProviderRequest) -> AsyncIterator[ProviderStreamEvent]:
|
|
55
56
|
api_key = self.require_api_key(request.api_key)
|
|
56
57
|
client = genai.Client(api_key=api_key, http_options=self._http_options(request.api_base))
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from collections.abc import AsyncIterator
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, override
|
|
9
9
|
|
|
10
10
|
from openai import APIError, AsyncOpenAI
|
|
11
11
|
|
|
@@ -42,6 +42,7 @@ class OpenAIChatAdapter(ProviderAdapter):
|
|
|
42
42
|
env_api_key_names = ("OPENAI_API_KEY",)
|
|
43
43
|
auto_discoverable = False
|
|
44
44
|
|
|
45
|
+
@override
|
|
45
46
|
async def stream_turn(self, request: ProviderRequest) -> AsyncIterator[ProviderStreamEvent]:
|
|
46
47
|
api_key = self.require_api_key(request.api_key)
|
|
47
48
|
client = AsyncOpenAI(
|
|
@@ -50,8 +51,6 @@ class OpenAIChatAdapter(ProviderAdapter):
|
|
|
50
51
|
timeout=DEFAULT_REQUEST_TIMEOUT,
|
|
51
52
|
)
|
|
52
53
|
|
|
53
|
-
# Keep the streamed turn state local to this adapter so the wire-format
|
|
54
|
-
# mapping stays readable in one file.
|
|
55
54
|
tool_calls: dict[int, _ChatToolCallState] = {}
|
|
56
55
|
text_parts: list[str] = []
|
|
57
56
|
thinking_parts: list[str] = []
|
|
@@ -338,6 +337,7 @@ class DeepSeekAdapter(OpenAIChatAdapter):
|
|
|
338
337
|
auto_discoverable = True
|
|
339
338
|
supports_reasoning_effort = True
|
|
340
339
|
|
|
340
|
+
@override
|
|
341
341
|
def _build_provider_payload_overrides(self, request: ProviderRequest) -> dict[str, Any]:
|
|
342
342
|
if request.reasoning_effort == "none":
|
|
343
343
|
return {"extra_body": {"thinking": {"type": "disabled"}}}
|
|
@@ -353,9 +353,8 @@ class DeepSeekAdapter(OpenAIChatAdapter):
|
|
|
353
353
|
class ZAIAdapter(OpenAIChatAdapter):
|
|
354
354
|
"""Z.AI's OpenAI-compatible chat endpoint.
|
|
355
355
|
|
|
356
|
-
GLM models think by default.
|
|
357
|
-
so
|
|
358
|
-
instead of resetting it on each turn.
|
|
356
|
+
GLM models think by default. The explicit thinking parameter is sent only
|
|
357
|
+
so ``clear_thinking=False`` preserves reasoning across multi-turn tool loops.
|
|
359
358
|
"""
|
|
360
359
|
|
|
361
360
|
provider_id = "zai"
|
|
@@ -365,6 +364,7 @@ class ZAIAdapter(OpenAIChatAdapter):
|
|
|
365
364
|
default_models = ("glm-5.1", "glm-5-turbo")
|
|
366
365
|
auto_discoverable = True
|
|
367
366
|
|
|
367
|
+
@override
|
|
368
368
|
def _build_provider_payload_overrides(self, request: ProviderRequest) -> dict[str, Any]:
|
|
369
369
|
return {"extra_body": {"thinking": {"type": "enabled", "clear_thinking": False}}}
|
|
370
370
|
|
|
@@ -380,6 +380,7 @@ class OpenRouterAdapter(OpenAIChatAdapter):
|
|
|
380
380
|
auto_discoverable = True
|
|
381
381
|
supports_reasoning_effort = True
|
|
382
382
|
|
|
383
|
+
@override
|
|
383
384
|
def _build_provider_payload_overrides(self, request: ProviderRequest) -> dict[str, Any]:
|
|
384
385
|
if not request.reasoning_effort:
|
|
385
386
|
return {}
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
from collections.abc import AsyncIterator
|
|
7
7
|
from copy import deepcopy
|
|
8
|
-
from typing import Any, cast
|
|
8
|
+
from typing import Any, cast, override
|
|
9
9
|
|
|
10
10
|
from openai import APIError, AsyncOpenAI
|
|
11
11
|
|
|
@@ -33,6 +33,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
|
|
|
33
33
|
default_models = ("gpt-5.5", "gpt-5.4-mini")
|
|
34
34
|
supports_reasoning_effort = True
|
|
35
35
|
|
|
36
|
+
@override
|
|
36
37
|
async def stream_turn(self, request: ProviderRequest) -> AsyncIterator[ProviderStreamEvent]:
|
|
37
38
|
api_key = self.require_api_key(request.api_key)
|
|
38
39
|
client = AsyncOpenAI(
|
|
@@ -20,23 +20,19 @@ from typing import TypedDict, cast
|
|
|
20
20
|
|
|
21
21
|
from mycode.messages import ConversationMessage, flatten_message_text
|
|
22
22
|
|
|
23
|
-
# ---------------------------------------------------------------------
|
|
24
|
-
# Session format defaults
|
|
25
|
-
# ---------------------------------------------------------------------
|
|
26
|
-
|
|
27
23
|
MESSAGE_FORMAT_VERSION = 7
|
|
28
24
|
DEFAULT_SESSION_TITLE = "New chat"
|
|
29
25
|
|
|
30
26
|
|
|
31
|
-
# ---------------------------------------------------------------------
|
|
32
|
-
# Rewind session events
|
|
33
|
-
# ---------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
|
|
36
27
|
def _now() -> str:
|
|
37
28
|
return datetime.now(UTC).isoformat()
|
|
38
29
|
|
|
39
30
|
|
|
31
|
+
# ---------------------------------------------------------------------
|
|
32
|
+
# Rewind markers
|
|
33
|
+
# ---------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
|
|
40
36
|
def build_rewind_event(rewind_to: int) -> ConversationMessage:
|
|
41
37
|
"""Build a rewind marker to append to session JSONL."""
|
|
42
38
|
|
|
@@ -112,7 +108,7 @@ class SessionStore:
|
|
|
112
108
|
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
113
109
|
|
|
114
110
|
# ---------------------------------------------------------------------
|
|
115
|
-
#
|
|
111
|
+
# Paths and meta I/O
|
|
116
112
|
# ---------------------------------------------------------------------
|
|
117
113
|
|
|
118
114
|
def session_dir(self, session_id: str) -> Path:
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
"""Tool execution runtime.
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Every tool runner receives a :class:`ToolContext` and a raw argument dict.
|
|
9
|
-
Context exposes the runtime state (cwd, output directory, image support) plus
|
|
10
|
-
typed facades for the four built-ins so custom tools can invoke them without
|
|
11
|
-
stringly-typed dispatch::
|
|
3
|
+
The four built-in tools (``read`` / ``write`` / ``edit`` / ``bash``) are
|
|
4
|
+
bundled as :data:`DEFAULT_TOOL_SPECS`. Custom tools wrap a typed Python
|
|
5
|
+
function with :func:`tool` and can invoke the built-ins through the facades
|
|
6
|
+
on :class:`ToolContext`::
|
|
12
7
|
|
|
13
8
|
@tool
|
|
14
9
|
def smart_read(ctx: ToolContext, path: str) -> str:
|
|
@@ -42,11 +37,10 @@ from mycode.messages import image_block, text_block
|
|
|
42
37
|
# ---------------------------------------------------------------------------
|
|
43
38
|
# Limits
|
|
44
39
|
# ---------------------------------------------------------------------------
|
|
45
|
-
|
|
40
|
+
|
|
46
41
|
DEFAULT_MAX_LINES = 2000
|
|
47
42
|
DEFAULT_MAX_BYTES = 50 * 1024
|
|
48
43
|
READ_MAX_LINE_CHARS = 2000
|
|
49
|
-
# bash
|
|
50
44
|
BASH_TIMEOUT_SECONDS = 120
|
|
51
45
|
_BASH_MAX_IN_MEMORY_BYTES = 5_000_000
|
|
52
46
|
|
|
@@ -60,20 +54,13 @@ ToolOutputCallback = Callable[[str], None]
|
|
|
60
54
|
|
|
61
55
|
@dataclass(frozen=True)
|
|
62
56
|
class ToolExecutionResult:
|
|
63
|
-
"""Result of one tool run.
|
|
64
|
-
|
|
65
|
-
Two consumers read this:
|
|
66
|
-
|
|
67
|
-
- **provider** — gets ``output`` (a text summary) and, for multimodal
|
|
68
|
-
returns, ``content`` (structured blocks such as an image). ``output``
|
|
69
|
-
is replayed on later turns.
|
|
70
|
-
- **UI** — reads ``metadata`` for structured rendering (e.g. edit patch
|
|
71
|
-
and stats). When ``metadata`` is absent, the UI falls back to ``output``
|
|
72
|
-
as a one-line summary.
|
|
73
|
-
"""
|
|
57
|
+
"""Result of one tool run."""
|
|
74
58
|
|
|
59
|
+
# Replayed to providers on later turns.
|
|
75
60
|
output: str
|
|
61
|
+
# Structured replay content, e.g. an image returned by a tool.
|
|
76
62
|
content: list[dict[str, Any]] | None = None
|
|
63
|
+
# UI-only structured data such as edit patches and line stats.
|
|
77
64
|
metadata: dict[str, Any] | None = None
|
|
78
65
|
is_error: bool = False
|
|
79
66
|
|
|
@@ -83,18 +70,13 @@ ToolRunner = Callable[["ToolContext", dict[str, Any]], ToolExecutionResult]
|
|
|
83
70
|
|
|
84
71
|
@dataclass(frozen=True)
|
|
85
72
|
class ToolSpec:
|
|
86
|
-
"""One tool the agent can call.
|
|
87
|
-
|
|
88
|
-
``runner`` receives a :class:`ToolContext` and the raw argument dict.
|
|
89
|
-
Tools that emit incremental output (``bash``) set ``streams_output=True``
|
|
90
|
-
and push lines via ``ctx.emit``; the agent forwards them as
|
|
91
|
-
``tool_output`` events live during execution.
|
|
92
|
-
"""
|
|
73
|
+
"""One tool the agent can call."""
|
|
93
74
|
|
|
94
75
|
name: str
|
|
95
76
|
description: str
|
|
96
77
|
input_schema: dict[str, Any]
|
|
97
78
|
runner: ToolRunner
|
|
79
|
+
# Streaming tools push incremental output through ToolContext.emit.
|
|
98
80
|
streams_output: bool = False
|
|
99
81
|
|
|
100
82
|
|
|
@@ -107,26 +89,17 @@ class ToolSpec:
|
|
|
107
89
|
class ToolContext:
|
|
108
90
|
"""Per-call runtime context handed to every tool runner.
|
|
109
91
|
|
|
110
|
-
|
|
111
|
-
the tool runs and what the model accepts. ``tool_call_id`` and ``emit``
|
|
112
|
-
are set by the agent for live streaming; they are ``None`` when a tool
|
|
113
|
-
is invoked outside the agent loop (e.g. TUI attachment read).
|
|
114
|
-
|
|
115
|
-
The four facades :meth:`read`, :meth:`write`, :meth:`edit`, :meth:`bash`
|
|
116
|
-
are the SDK-friendly way to invoke the built-in tools from custom code.
|
|
117
|
-
:meth:`call` is a generic fallback for dispatching to any registered
|
|
118
|
-
tool by name.
|
|
92
|
+
Includes typed facades for built-in tools and generic registry dispatch.
|
|
119
93
|
"""
|
|
120
94
|
|
|
121
95
|
executor: ToolExecutor
|
|
122
96
|
cwd: str
|
|
123
97
|
tool_output_dir: Path
|
|
124
98
|
supports_image_input: bool = False
|
|
99
|
+
# Only agent-loop calls have a provider tool-call id and live output sink.
|
|
125
100
|
tool_call_id: str | None = None
|
|
126
101
|
emit: ToolOutputCallback | None = None
|
|
127
102
|
|
|
128
|
-
# Typed facades for the built-ins.
|
|
129
|
-
|
|
130
103
|
def read(
|
|
131
104
|
self,
|
|
132
105
|
path: str,
|
|
@@ -146,14 +119,10 @@ class ToolContext:
|
|
|
146
119
|
return self.call("bash", {"command": command, "timeout": timeout})
|
|
147
120
|
|
|
148
121
|
def call(self, name: str, args: dict[str, Any]) -> ToolExecutionResult:
|
|
149
|
-
"""Dispatch
|
|
150
|
-
custom tools that wrap another registered tool."""
|
|
122
|
+
"""Dispatch through the registry, including from custom tool wrappers."""
|
|
151
123
|
|
|
152
124
|
return self.executor.execute(name, args, self)
|
|
153
125
|
|
|
154
|
-
# Subprocess hooks — used by bash to register/unregister its child so
|
|
155
|
-
# ``Agent.cancel`` and ``cancel_all_tools`` can terminate it.
|
|
156
|
-
|
|
157
126
|
def track_proc(self, proc: subprocess.Popen[str]) -> None:
|
|
158
127
|
self.executor.track_proc(proc)
|
|
159
128
|
|
|
@@ -167,12 +136,7 @@ class ToolContext:
|
|
|
167
136
|
|
|
168
137
|
|
|
169
138
|
class ToolExecutor:
|
|
170
|
-
"""Tool registry plus subprocess lifecycle for one agent session.
|
|
171
|
-
|
|
172
|
-
Agents construct one executor internally; SDK users normally pass
|
|
173
|
-
``tools=[...]`` to :class:`~mycode.agent.Agent` rather than building
|
|
174
|
-
this directly.
|
|
175
|
-
"""
|
|
139
|
+
"""Tool registry plus subprocess lifecycle for one agent session."""
|
|
176
140
|
|
|
177
141
|
def __init__(self, tools: Sequence[ToolSpec]):
|
|
178
142
|
names = [spec.name for spec in tools]
|
|
@@ -184,8 +148,6 @@ class ToolExecutor:
|
|
|
184
148
|
|
|
185
149
|
@property
|
|
186
150
|
def definitions(self) -> list[dict[str, Any]]:
|
|
187
|
-
"""Provider-facing tool definitions (name / description / schema)."""
|
|
188
|
-
|
|
189
151
|
return [
|
|
190
152
|
{"name": spec.name, "description": spec.description, "input_schema": spec.input_schema}
|
|
191
153
|
for spec in self._tools.values()
|
|
@@ -230,10 +192,9 @@ class ToolExecutor:
|
|
|
230
192
|
# ---------------------------------------------------------------------------
|
|
231
193
|
# Subprocess lifecycle (process-global)
|
|
232
194
|
# ---------------------------------------------------------------------------
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
# safety net exposed as ``cancel_all_tools``.
|
|
195
|
+
# The executor's tracking set scopes cancellation to one session (multiple
|
|
196
|
+
# agents may run concurrently in the same process); this module-level set is
|
|
197
|
+
# a shutdown-time safety net exposed as ``cancel_all_tools``.
|
|
237
198
|
|
|
238
199
|
_ACTIVE_PROCS: set[subprocess.Popen[str]] = set()
|
|
239
200
|
_ACTIVE_PROCS_LOCK = threading.Lock()
|
|
@@ -580,8 +541,6 @@ def _run_edit(ctx: ToolContext, args: dict[str, Any]) -> ToolExecutionResult:
|
|
|
580
541
|
is_error=True,
|
|
581
542
|
)
|
|
582
543
|
|
|
583
|
-
# Fuzzy fallback: normalize both sides, find in normalized space,
|
|
584
|
-
# but map the span back to the original text for replacement.
|
|
585
544
|
if norm_text is None:
|
|
586
545
|
norm_text, norm_imap = _normalize_text(text)
|
|
587
546
|
norm_old, _ = _normalize_text(old_text)
|
|
@@ -604,6 +563,8 @@ def _run_edit(ctx: ToolContext, args: dict[str, Any]) -> ToolExecutionResult:
|
|
|
604
563
|
|
|
605
564
|
idx = norm_text.find(norm_old)
|
|
606
565
|
assert norm_imap is not None
|
|
566
|
+
# Normalized matching tolerates whitespace drift; replacement still
|
|
567
|
+
# uses original offsets so untouched content is preserved exactly.
|
|
607
568
|
orig_start = norm_imap[idx]
|
|
608
569
|
end_idx = idx + len(norm_old)
|
|
609
570
|
orig_end = norm_imap[end_idx] if end_idx < len(norm_imap) else len(text)
|
|
@@ -1009,14 +970,11 @@ def tool(
|
|
|
1009
970
|
description: str | None = None,
|
|
1010
971
|
streams_output: bool = False,
|
|
1011
972
|
) -> ToolSpec | Callable[[Callable[..., Any]], ToolSpec]:
|
|
1012
|
-
"""Wrap a
|
|
1013
|
-
|
|
1014
|
-
Sync and async functions are both supported. If the first parameter is
|
|
1015
|
-
annotated :class:`ToolContext`, the context is injected automatically
|
|
1016
|
-
and the remaining parameters drive the JSON schema sent to the provider.
|
|
973
|
+
"""Wrap a sync or async Python function as a :class:`ToolSpec`.
|
|
1017
974
|
|
|
1018
|
-
|
|
1019
|
-
|
|
975
|
+
A first parameter annotated :class:`ToolContext` is injected automatically;
|
|
976
|
+
the remaining parameters drive the input schema. The function may return a
|
|
977
|
+
:class:`ToolExecutionResult` or any JSON-serializable value.
|
|
1020
978
|
"""
|
|
1021
979
|
|
|
1022
980
|
def wrap(fn: Callable[..., Any]) -> ToolSpec:
|
|
@@ -1026,6 +984,8 @@ def tool(
|
|
|
1026
984
|
except Exception:
|
|
1027
985
|
resolved_hints = {}
|
|
1028
986
|
wants_context = bool(parameters) and resolved_hints.get(parameters[0].name) is ToolContext
|
|
987
|
+
# ToolContext is injected locally and must not appear in the provider
|
|
988
|
+
# input schema.
|
|
1029
989
|
tool_params = parameters[1:] if wants_context else parameters
|
|
1030
990
|
param_names = {p.name for p in tool_params}
|
|
1031
991
|
input_schema, coercions = _build_input_schema(tool_params, resolved_hints)
|
|
@@ -1070,7 +1030,7 @@ def _build_input_schema(
|
|
|
1070
1030
|
) -> tuple[dict[str, Any], dict[str, Callable[[Any], Any]]]:
|
|
1071
1031
|
"""Build the JSON schema for ``parameters`` and a per-name coercion map.
|
|
1072
1032
|
|
|
1073
|
-
The coercion map carries post-JSON conversions for annotations
|
|
1033
|
+
The coercion map carries post-JSON conversions for annotations without a
|
|
1074
1034
|
native JSON type (currently only ``Path``).
|
|
1075
1035
|
"""
|
|
1076
1036
|
|
|
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
|