mycode-sdk 0.9.0__tar.gz → 0.9.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.
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/PKG-INFO +6 -6
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/pyproject.toml +6 -6
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/__init__.py +1 -8
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/agent.py +79 -87
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/attachments.py +3 -2
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/providers/anthropic_like.py +25 -24
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/providers/base.py +6 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/providers/gemini.py +2 -3
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/providers/openai_chat.py +6 -3
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/providers/openai_responses.py +4 -3
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/session.py +11 -10
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/tools.py +10 -12
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/.gitignore +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/LICENSE +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/README.md +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/compact.py +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/hooks.py +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/messages.py +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/models.py +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/models_catalog.json +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.1}/src/mycode/py.typed +0 -0
- {mycode_sdk-0.9.0 → mycode_sdk-0.9.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.9.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -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.
|
|
22
|
-
Requires-Dist: google-genai>=2.
|
|
23
|
-
Requires-Dist: griffelib>=2.0.
|
|
24
|
-
Requires-Dist: openai>=2.
|
|
25
|
-
Requires-Dist: pydantic>=2.13.
|
|
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
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
28
|
# mycode-sdk
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mycode-sdk"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.1"
|
|
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.
|
|
27
|
-
"google-genai>=2.
|
|
28
|
-
"griffelib>=2.0.
|
|
29
|
-
"openai>=2.
|
|
30
|
-
"pydantic>=2.13.
|
|
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",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.urls]
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
"""mycode — multi-turn tool-calling agent runtime.
|
|
2
|
-
|
|
3
|
-
Public API for embedding the agent loop in other Python applications. The
|
|
4
|
-
runtime ships four built-in coding tools (``read``, ``write``, ``edit``,
|
|
5
|
-
``bash``) exposed as :data:`read_tool`, :data:`write_tool`, :data:`edit_tool`,
|
|
6
|
-
:data:`bash_tool` — pick the ones you want via ``tools=[...]`` rather than
|
|
7
|
-
silently exposing file system and shell access.
|
|
8
|
-
"""
|
|
1
|
+
"""mycode — multi-turn tool-calling agent runtime."""
|
|
9
2
|
|
|
10
3
|
from importlib import metadata
|
|
11
4
|
|
|
@@ -13,6 +13,7 @@ import os
|
|
|
13
13
|
import tempfile
|
|
14
14
|
import time
|
|
15
15
|
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
|
16
|
+
from contextlib import suppress
|
|
16
17
|
from dataclasses import dataclass, field
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any, cast
|
|
@@ -232,18 +233,12 @@ class Agent:
|
|
|
232
233
|
yield Event("tool_start", {"tool_call": {"id": tool_id, "name": name, "input": args}})
|
|
233
234
|
|
|
234
235
|
if self._cancel_event.is_set():
|
|
235
|
-
yield self.
|
|
236
|
-
tool_id,
|
|
237
|
-
ToolExecutionResult(output="error: cancelled", is_error=True),
|
|
238
|
-
)
|
|
236
|
+
yield self._error_done(tool_id, "error: cancelled")
|
|
239
237
|
return
|
|
240
238
|
|
|
241
239
|
spec = self.tools.get(name)
|
|
242
240
|
if spec is None:
|
|
243
|
-
yield self.
|
|
244
|
-
tool_id,
|
|
245
|
-
ToolExecutionResult(output=f"error: unknown tool: {name}", is_error=True),
|
|
246
|
-
)
|
|
241
|
+
yield self._error_done(tool_id, f"error: unknown tool: {name}")
|
|
247
242
|
return
|
|
248
243
|
|
|
249
244
|
hook_ctx = ToolHookContext(
|
|
@@ -259,10 +254,7 @@ class Agent:
|
|
|
259
254
|
try:
|
|
260
255
|
result = await self.hooks.run_before_tool(hook_ctx)
|
|
261
256
|
except Exception as exc:
|
|
262
|
-
yield self.
|
|
263
|
-
tool_id,
|
|
264
|
-
ToolExecutionResult(output=f"error: tool hook failed: {exc}", is_error=True),
|
|
265
|
-
)
|
|
257
|
+
yield self._error_done(tool_id, f"error: tool hook failed: {exc}")
|
|
266
258
|
return
|
|
267
259
|
|
|
268
260
|
if result is not None:
|
|
@@ -270,10 +262,7 @@ class Agent:
|
|
|
270
262
|
return
|
|
271
263
|
|
|
272
264
|
if self._cancel_event.is_set():
|
|
273
|
-
yield self.
|
|
274
|
-
tool_id,
|
|
275
|
-
ToolExecutionResult(output="error: cancelled", is_error=True),
|
|
276
|
-
)
|
|
265
|
+
yield self._error_done(tool_id, "error: cancelled")
|
|
277
266
|
return
|
|
278
267
|
|
|
279
268
|
if spec.streams_output:
|
|
@@ -336,13 +325,10 @@ class Agent:
|
|
|
336
325
|
yield Event("tool_output", {"tool_use_id": tool_id, "output": output})
|
|
337
326
|
|
|
338
327
|
if was_cancelled:
|
|
339
|
-
|
|
328
|
+
with suppress(Exception):
|
|
340
329
|
await task
|
|
341
|
-
except Exception:
|
|
342
|
-
pass
|
|
343
330
|
output = "\n".join([*output_parts, "error: cancelled"]) if output_parts else "error: cancelled"
|
|
344
|
-
|
|
345
|
-
yield self._tool_done_event(tool_id, result)
|
|
331
|
+
yield self._error_done(tool_id, output)
|
|
346
332
|
return
|
|
347
333
|
else:
|
|
348
334
|
try:
|
|
@@ -398,6 +384,11 @@ class Agent:
|
|
|
398
384
|
data["content"] = result.content
|
|
399
385
|
return Event("tool_done", data)
|
|
400
386
|
|
|
387
|
+
def _error_done(self, tool_id: str, message: str) -> Event:
|
|
388
|
+
"""Build a tool_done event carrying an error result."""
|
|
389
|
+
|
|
390
|
+
return self._tool_done_event(tool_id, ToolExecutionResult(output=message, is_error=True))
|
|
391
|
+
|
|
401
392
|
# ------------------------------------------------------------------
|
|
402
393
|
# Provider streaming
|
|
403
394
|
# ------------------------------------------------------------------
|
|
@@ -429,10 +420,52 @@ class Agent:
|
|
|
429
420
|
finally:
|
|
430
421
|
close = cast(Callable[[], Awaitable[None]] | None, getattr(provider_stream, "aclose", None))
|
|
431
422
|
if close is not None:
|
|
432
|
-
|
|
423
|
+
with suppress(Exception):
|
|
433
424
|
await close()
|
|
434
|
-
|
|
435
|
-
|
|
425
|
+
|
|
426
|
+
def _build_request(
|
|
427
|
+
self,
|
|
428
|
+
*,
|
|
429
|
+
tools: list[dict[str, Any]] | None = None,
|
|
430
|
+
max_tokens: int | None = None,
|
|
431
|
+
reasoning_effort: str | None = None,
|
|
432
|
+
append_messages: Sequence[ConversationMessage] = (),
|
|
433
|
+
) -> ProviderRequest:
|
|
434
|
+
"""Build a ProviderRequest from the agent's current state."""
|
|
435
|
+
|
|
436
|
+
return ProviderRequest(
|
|
437
|
+
provider=self.provider,
|
|
438
|
+
model=self.model,
|
|
439
|
+
session_id=self.session_id,
|
|
440
|
+
messages=self.messages,
|
|
441
|
+
system=self.system,
|
|
442
|
+
tools=self.tools.definitions if tools is None else tools,
|
|
443
|
+
max_tokens=self.max_tokens if max_tokens is None else max_tokens,
|
|
444
|
+
temperature=self.temperature,
|
|
445
|
+
api_key=self.api_key,
|
|
446
|
+
api_base=self.api_base,
|
|
447
|
+
reasoning_effort=reasoning_effort,
|
|
448
|
+
supports_image_input=self.supports_image_input,
|
|
449
|
+
supports_pdf_input=self.supports_pdf_input,
|
|
450
|
+
transcript_path=self.transcript_path,
|
|
451
|
+
append_messages=list(append_messages),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
@staticmethod
|
|
455
|
+
def _elapsed_ms(start: float) -> int:
|
|
456
|
+
return max(0, int((time.monotonic() - start) * 1000))
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def _stamp_thinking_duration(content: list[Any], duration_ms: int) -> None:
|
|
460
|
+
"""Merge duration_ms onto the last thinking block in content, if any."""
|
|
461
|
+
|
|
462
|
+
for block in reversed(content):
|
|
463
|
+
if not isinstance(block, dict) or block.get("type") != "thinking":
|
|
464
|
+
continue
|
|
465
|
+
raw_meta = block.get("meta")
|
|
466
|
+
meta = raw_meta if isinstance(raw_meta, dict) else {}
|
|
467
|
+
block["meta"] = {**meta, "duration_ms": duration_ms}
|
|
468
|
+
return
|
|
436
469
|
|
|
437
470
|
# ------------------------------------------------------------------
|
|
438
471
|
# Public entry points
|
|
@@ -460,8 +493,9 @@ class Agent:
|
|
|
460
493
|
|
|
461
494
|
self._cancel_event.clear()
|
|
462
495
|
|
|
496
|
+
user_message: ConversationMessage
|
|
463
497
|
if isinstance(user_input, str):
|
|
464
|
-
user_message
|
|
498
|
+
user_message = user_text_message(user_input)
|
|
465
499
|
else:
|
|
466
500
|
if (user_input.get("role") or "user") != "user":
|
|
467
501
|
yield Event("error", {"message": "user input must be a user message"})
|
|
@@ -479,16 +513,15 @@ class Agent:
|
|
|
479
513
|
user_message["content"] = list(user_message.get("content") or []) + blocks
|
|
480
514
|
|
|
481
515
|
content_blocks = user_message.get("content") or []
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
yield Event("error", {"message": "current model does not support image input"})
|
|
486
|
-
return
|
|
487
|
-
if not self.supports_pdf_input and any(
|
|
488
|
-
isinstance(block, dict) and block.get("type") == "document" for block in content_blocks
|
|
516
|
+
for block_type, supported, label in (
|
|
517
|
+
("image", self.supports_image_input, "image input"),
|
|
518
|
+
("document", self.supports_pdf_input, "PDF input"),
|
|
489
519
|
):
|
|
490
|
-
|
|
491
|
-
|
|
520
|
+
if not supported and any(
|
|
521
|
+
isinstance(block, dict) and block.get("type") == block_type for block in content_blocks
|
|
522
|
+
):
|
|
523
|
+
yield Event("error", {"message": f"current model does not support {label}"})
|
|
524
|
+
return
|
|
492
525
|
|
|
493
526
|
self.messages.append(user_message)
|
|
494
527
|
await persist(user_message)
|
|
@@ -507,22 +540,7 @@ class Agent:
|
|
|
507
540
|
thinking_started_at: float | None = None
|
|
508
541
|
thinking_duration_ms: int | None = None
|
|
509
542
|
provider_cancelled = False
|
|
510
|
-
request =
|
|
511
|
-
provider=self.provider,
|
|
512
|
-
model=self.model,
|
|
513
|
-
session_id=self.session_id,
|
|
514
|
-
messages=self.messages,
|
|
515
|
-
system=self.system,
|
|
516
|
-
tools=self.tools.definitions,
|
|
517
|
-
max_tokens=self.max_tokens,
|
|
518
|
-
temperature=self.temperature,
|
|
519
|
-
api_key=self.api_key,
|
|
520
|
-
api_base=self.api_base,
|
|
521
|
-
reasoning_effort=self.reasoning_effort,
|
|
522
|
-
supports_image_input=self.supports_image_input,
|
|
523
|
-
supports_pdf_input=self.supports_pdf_input,
|
|
524
|
-
transcript_path=self.transcript_path,
|
|
525
|
-
)
|
|
543
|
+
request = self._build_request(reasoning_effort=self.reasoning_effort)
|
|
526
544
|
|
|
527
545
|
try:
|
|
528
546
|
async for provider_event in self._stream_provider_turn(adapter, request):
|
|
@@ -546,7 +564,7 @@ class Agent:
|
|
|
546
564
|
delta_text = str(provider_event.data.get("text") or "")
|
|
547
565
|
if delta_text:
|
|
548
566
|
if thinking_started_at is not None and thinking_duration_ms is None:
|
|
549
|
-
thinking_duration_ms =
|
|
567
|
+
thinking_duration_ms = self._elapsed_ms(thinking_started_at)
|
|
550
568
|
yield Event("reasoning_done", {"duration_ms": thinking_duration_ms})
|
|
551
569
|
if partial_content and partial_content[-1].get("type") == "text":
|
|
552
570
|
partial_content[-1]["text"] = f"{partial_content[-1].get('text') or ''}{delta_text}"
|
|
@@ -562,7 +580,7 @@ class Agent:
|
|
|
562
580
|
continue
|
|
563
581
|
|
|
564
582
|
if thinking_started_at is not None and thinking_duration_ms is None:
|
|
565
|
-
thinking_duration_ms =
|
|
583
|
+
thinking_duration_ms = self._elapsed_ms(thinking_started_at)
|
|
566
584
|
yield Event("reasoning_done", {"duration_ms": thinking_duration_ms})
|
|
567
585
|
|
|
568
586
|
message = provider_event.data.get("message")
|
|
@@ -579,15 +597,9 @@ class Agent:
|
|
|
579
597
|
if provider_cancelled:
|
|
580
598
|
if partial_content:
|
|
581
599
|
if thinking_started_at is not None and thinking_duration_ms is None:
|
|
582
|
-
thinking_duration_ms =
|
|
600
|
+
thinking_duration_ms = self._elapsed_ms(thinking_started_at)
|
|
583
601
|
if thinking_duration_ms is not None:
|
|
584
|
-
|
|
585
|
-
if block.get("type") != "thinking":
|
|
586
|
-
continue
|
|
587
|
-
raw_meta = block.get("meta")
|
|
588
|
-
meta = cast(dict[str, Any], raw_meta) if isinstance(raw_meta, dict) else {}
|
|
589
|
-
block["meta"] = {**meta, "duration_ms": thinking_duration_ms}
|
|
590
|
-
break
|
|
602
|
+
self._stamp_thinking_duration(partial_content, thinking_duration_ms)
|
|
591
603
|
cancelled_message = build_message(
|
|
592
604
|
"assistant",
|
|
593
605
|
[dict(block) for block in partial_content],
|
|
@@ -607,13 +619,7 @@ class Agent:
|
|
|
607
619
|
return
|
|
608
620
|
|
|
609
621
|
if thinking_duration_ms is not None:
|
|
610
|
-
|
|
611
|
-
if not isinstance(block, dict) or block.get("type") != "thinking":
|
|
612
|
-
continue
|
|
613
|
-
raw_meta = block.get("meta")
|
|
614
|
-
meta = cast(dict[str, Any], raw_meta) if isinstance(raw_meta, dict) else {}
|
|
615
|
-
block["meta"] = {**meta, "duration_ms": thinking_duration_ms}
|
|
616
|
-
break
|
|
622
|
+
self._stamp_thinking_duration(assistant_message.get("content") or [], thinking_duration_ms)
|
|
617
623
|
|
|
618
624
|
# Stamp context_window onto the persisted assistant message so
|
|
619
625
|
# rewinds and refreshed clients can render token-usage % without
|
|
@@ -649,16 +655,13 @@ class Agent:
|
|
|
649
655
|
continue
|
|
650
656
|
|
|
651
657
|
d = event.data
|
|
652
|
-
output = str(d.get("output") or "")
|
|
653
|
-
metadata = d.get("metadata") if isinstance(d.get("metadata"), dict) else None
|
|
654
|
-
content = d.get("content")
|
|
655
658
|
tool_results.append(
|
|
656
659
|
tool_result_block(
|
|
657
|
-
tool_use_id=
|
|
658
|
-
output=output,
|
|
659
|
-
metadata=metadata,
|
|
660
|
-
is_error=
|
|
661
|
-
content=
|
|
660
|
+
tool_use_id=d["tool_use_id"],
|
|
661
|
+
output=d["output"],
|
|
662
|
+
metadata=d.get("metadata"),
|
|
663
|
+
is_error=d["is_error"],
|
|
664
|
+
content=d.get("content"),
|
|
662
665
|
)
|
|
663
666
|
)
|
|
664
667
|
|
|
@@ -735,20 +738,9 @@ class Agent:
|
|
|
735
738
|
) -> None:
|
|
736
739
|
"""Ask the provider for a summary, persist the compact event, append it."""
|
|
737
740
|
|
|
738
|
-
request =
|
|
739
|
-
provider=self.provider,
|
|
740
|
-
model=self.model,
|
|
741
|
-
session_id=self.session_id,
|
|
742
|
-
messages=self.messages,
|
|
743
|
-
system=self.system,
|
|
741
|
+
request = self._build_request(
|
|
744
742
|
tools=[],
|
|
745
743
|
max_tokens=min(self.max_tokens, 8192),
|
|
746
|
-
temperature=self.temperature,
|
|
747
|
-
api_key=self.api_key,
|
|
748
|
-
api_base=self.api_base,
|
|
749
|
-
supports_image_input=self.supports_image_input,
|
|
750
|
-
supports_pdf_input=self.supports_pdf_input,
|
|
751
|
-
transcript_path=self.transcript_path,
|
|
752
744
|
append_messages=[user_text_message(COMPACT_SUMMARY_PROMPT)],
|
|
753
745
|
)
|
|
754
746
|
|
|
@@ -6,6 +6,7 @@ same content blocks for the same input.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import builtins
|
|
9
10
|
import html
|
|
10
11
|
from base64 import b64encode
|
|
11
12
|
from collections.abc import Sequence
|
|
@@ -29,7 +30,7 @@ class Attachment:
|
|
|
29
30
|
bare ``Attachment("notes.txt")`` from silently becoming text instead of a path.
|
|
30
31
|
"""
|
|
31
32
|
|
|
32
|
-
source: Path | bytes | str
|
|
33
|
+
source: Path | builtins.bytes | str
|
|
33
34
|
media_type: str | None = None
|
|
34
35
|
name: str | None = None
|
|
35
36
|
|
|
@@ -40,7 +41,7 @@ class Attachment:
|
|
|
40
41
|
return cls(source=Path(path), name=name)
|
|
41
42
|
|
|
42
43
|
@classmethod
|
|
43
|
-
def bytes(cls, data: bytes, *, media_type: str, name: str | None = None) -> Self:
|
|
44
|
+
def bytes(cls, data: builtins.bytes, *, media_type: str, name: str | None = None) -> Self:
|
|
44
45
|
"""``media_type`` must be a supported image type or ``application/pdf``."""
|
|
45
46
|
|
|
46
47
|
if not media_type:
|
|
@@ -24,6 +24,7 @@ from mycode.providers.base import (
|
|
|
24
24
|
get_native_meta,
|
|
25
25
|
load_document_block_payload,
|
|
26
26
|
load_image_block_payload,
|
|
27
|
+
native_block_meta,
|
|
27
28
|
tool_result_content_blocks,
|
|
28
29
|
)
|
|
29
30
|
|
|
@@ -133,25 +134,27 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
133
134
|
api_key = self.require_api_key(request.api_key)
|
|
134
135
|
|
|
135
136
|
try:
|
|
136
|
-
async with
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
137
|
+
async with (
|
|
138
|
+
AsyncAnthropic(
|
|
139
|
+
api_key=api_key,
|
|
140
|
+
base_url=self.resolve_base_url(request.api_base),
|
|
141
|
+
timeout=DEFAULT_REQUEST_TIMEOUT,
|
|
142
|
+
) as client,
|
|
143
|
+
client.messages.stream(**self._build_request_payload(request)) as stream,
|
|
144
|
+
):
|
|
145
|
+
async for event in stream:
|
|
146
|
+
event_type = getattr(event, "type", None)
|
|
147
|
+
if event_type == "thinking":
|
|
148
|
+
thinking = cast(str | None, getattr(event, "thinking", None))
|
|
149
|
+
if thinking:
|
|
150
|
+
yield ProviderStreamEvent("thinking_delta", {"text": thinking})
|
|
151
|
+
continue
|
|
152
|
+
if event_type == "text":
|
|
153
|
+
text = cast(str | None, getattr(event, "text", None))
|
|
154
|
+
if text:
|
|
155
|
+
yield ProviderStreamEvent("text_delta", {"text": text})
|
|
156
|
+
|
|
157
|
+
final_message = await stream.get_final_message()
|
|
155
158
|
except APIError as exc:
|
|
156
159
|
raise ValueError(str(exc)) from exc
|
|
157
160
|
|
|
@@ -175,7 +178,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
175
178
|
blocks.append(
|
|
176
179
|
thinking_block(
|
|
177
180
|
getattr(block, "thinking", ""),
|
|
178
|
-
meta=
|
|
181
|
+
meta=native_block_meta(native_meta),
|
|
179
182
|
)
|
|
180
183
|
)
|
|
181
184
|
continue
|
|
@@ -185,9 +188,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
185
188
|
citations = getattr(block, "citations", None)
|
|
186
189
|
if citations:
|
|
187
190
|
native_meta["citations"] = dump_model(citations)
|
|
188
|
-
blocks.append(
|
|
189
|
-
text_block(getattr(block, "text", ""), meta={"native": native_meta} if native_meta else None)
|
|
190
|
-
)
|
|
191
|
+
blocks.append(text_block(getattr(block, "text", ""), meta=native_block_meta(native_meta)))
|
|
191
192
|
continue
|
|
192
193
|
|
|
193
194
|
if block_type == "tool_use":
|
|
@@ -200,7 +201,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
200
201
|
tool_id=getattr(block, "id", ""),
|
|
201
202
|
name=getattr(block, "name", ""),
|
|
202
203
|
input=getattr(block, "input", None),
|
|
203
|
-
meta=
|
|
204
|
+
meta=native_block_meta(native_meta),
|
|
204
205
|
)
|
|
205
206
|
)
|
|
206
207
|
continue
|
|
@@ -68,6 +68,12 @@ def get_native_meta(block: dict[str, Any]) -> dict[str, Any]:
|
|
|
68
68
|
return {}
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def native_block_meta(native: dict[str, Any]) -> dict[str, Any] | None:
|
|
72
|
+
"""Wrap accumulated native values as block meta, or None when empty."""
|
|
73
|
+
|
|
74
|
+
return {"native": native} if native else None
|
|
75
|
+
|
|
76
|
+
|
|
71
77
|
class ProviderAdapter(ABC):
|
|
72
78
|
"""Base class for provider adapters.
|
|
73
79
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections.abc import AsyncIterator
|
|
6
|
+
from contextlib import suppress
|
|
6
7
|
from typing import Any, override
|
|
7
8
|
from urllib.parse import urlparse
|
|
8
9
|
|
|
@@ -88,10 +89,8 @@ class GoogleGeminiAdapter(ProviderAdapter):
|
|
|
88
89
|
except APIError as exc:
|
|
89
90
|
raise ValueError(str(exc)) from exc
|
|
90
91
|
finally:
|
|
91
|
-
|
|
92
|
+
with suppress(Exception):
|
|
92
93
|
await client.aio.aclose()
|
|
93
|
-
except Exception:
|
|
94
|
-
pass
|
|
95
94
|
|
|
96
95
|
raw_usage = usage or {}
|
|
97
96
|
total_tokens = raw_usage.get("total_token_count") or None
|
|
@@ -19,6 +19,7 @@ from mycode.providers.base import (
|
|
|
19
19
|
get_native_meta,
|
|
20
20
|
load_document_block_payload,
|
|
21
21
|
load_image_block_payload,
|
|
22
|
+
native_block_meta,
|
|
22
23
|
)
|
|
23
24
|
from mycode.utils import omit_none, parse_tool_arguments
|
|
24
25
|
|
|
@@ -109,7 +110,7 @@ class OpenAIChatAdapter(ProviderAdapter):
|
|
|
109
110
|
blocks.append(
|
|
110
111
|
thinking_block(
|
|
111
112
|
"".join(thinking_parts),
|
|
112
|
-
meta=
|
|
113
|
+
meta=native_block_meta(thinking_native_meta),
|
|
113
114
|
)
|
|
114
115
|
)
|
|
115
116
|
if text_parts:
|
|
@@ -248,13 +249,15 @@ class OpenAIChatAdapter(ProviderAdapter):
|
|
|
248
249
|
if role != "assistant":
|
|
249
250
|
return []
|
|
250
251
|
|
|
251
|
-
text_parts = [
|
|
252
|
+
text_parts = [
|
|
253
|
+
str(block.get("text") or "") for block in blocks if block.get("type") == "text" and block.get("text")
|
|
254
|
+
]
|
|
252
255
|
thinking_blocks = [block for block in blocks if block.get("type") == "thinking"]
|
|
253
256
|
tool_use_blocks = [block for block in blocks if block.get("type") == "tool_use"]
|
|
254
257
|
|
|
255
258
|
payload: dict[str, Any] = {
|
|
256
259
|
"role": "assistant",
|
|
257
|
-
"content": "\n".join(
|
|
260
|
+
"content": "\n".join(text_parts),
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
if tool_use_blocks:
|
|
@@ -18,6 +18,7 @@ from mycode.providers.base import (
|
|
|
18
18
|
dump_model,
|
|
19
19
|
load_document_block_payload,
|
|
20
20
|
load_image_block_payload,
|
|
21
|
+
native_block_meta,
|
|
21
22
|
tool_result_content_blocks,
|
|
22
23
|
)
|
|
23
24
|
from mycode.utils import omit_none, parse_tool_arguments
|
|
@@ -285,7 +286,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
|
|
|
285
286
|
blocks.append(
|
|
286
287
|
thinking_block(
|
|
287
288
|
"".join(text_parts),
|
|
288
|
-
meta=
|
|
289
|
+
meta=native_block_meta(item_meta),
|
|
289
290
|
)
|
|
290
291
|
)
|
|
291
292
|
continue
|
|
@@ -301,7 +302,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
|
|
|
301
302
|
blocks.append(
|
|
302
303
|
text_block(
|
|
303
304
|
getattr(part, "text", ""),
|
|
304
|
-
meta=
|
|
305
|
+
meta=native_block_meta(native_meta),
|
|
305
306
|
)
|
|
306
307
|
)
|
|
307
308
|
continue
|
|
@@ -327,7 +328,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
|
|
|
327
328
|
tool_id=getattr(item, "call_id", ""),
|
|
328
329
|
name=getattr(item, "name", ""),
|
|
329
330
|
input=tool_input,
|
|
330
|
-
meta=
|
|
331
|
+
meta=native_block_meta(item_meta),
|
|
331
332
|
)
|
|
332
333
|
)
|
|
333
334
|
|
|
@@ -85,6 +85,12 @@ SessionMetaDict = dict[str, object]
|
|
|
85
85
|
SessionIndex = dict[str, SessionMetaDict]
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def _project_meta(raw: dict[str, object]) -> SessionMetaDict:
|
|
89
|
+
"""Keep only the known session-meta keys."""
|
|
90
|
+
|
|
91
|
+
return {key: raw[key] for key in META_KEYS if key in raw}
|
|
92
|
+
|
|
93
|
+
|
|
88
94
|
class SessionData(TypedDict):
|
|
89
95
|
session: SessionMetaDict
|
|
90
96
|
messages: list[ConversationMessage]
|
|
@@ -138,10 +144,10 @@ class SessionStore:
|
|
|
138
144
|
return None
|
|
139
145
|
if not isinstance(raw, dict):
|
|
140
146
|
return None
|
|
141
|
-
return
|
|
147
|
+
return _project_meta(raw)
|
|
142
148
|
|
|
143
149
|
def _write_meta(self, session_id: str, meta: SessionMetaDict) -> None:
|
|
144
|
-
meta =
|
|
150
|
+
meta = _project_meta(meta)
|
|
145
151
|
self.meta_path(session_id).write_text(json.dumps(meta, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
146
152
|
index = self._read_index()
|
|
147
153
|
index[session_id] = dict(meta)
|
|
@@ -154,11 +160,7 @@ class SessionStore:
|
|
|
154
160
|
return self._rebuild_index()
|
|
155
161
|
if not isinstance(raw, dict):
|
|
156
162
|
return self._rebuild_index()
|
|
157
|
-
return {
|
|
158
|
-
str(session_id): {key: meta[key] for key in META_KEYS if key in meta}
|
|
159
|
-
for session_id, meta in raw.items()
|
|
160
|
-
if isinstance(meta, dict)
|
|
161
|
-
}
|
|
163
|
+
return {str(session_id): _project_meta(meta) for session_id, meta in raw.items() if isinstance(meta, dict)}
|
|
162
164
|
|
|
163
165
|
def _write_index(self, index: SessionIndex) -> None:
|
|
164
166
|
self.index_path().write_text(json.dumps(index, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
@@ -228,7 +230,6 @@ class SessionStore:
|
|
|
228
230
|
if meta is None:
|
|
229
231
|
return None
|
|
230
232
|
|
|
231
|
-
# Read the raw append-only log first. Replay happens after that.
|
|
232
233
|
raw_messages: list[ConversationMessage] = []
|
|
233
234
|
try:
|
|
234
235
|
with self.messages_path(session_id).open("r", encoding="utf-8") as handle:
|
|
@@ -238,10 +239,10 @@ class SessionStore:
|
|
|
238
239
|
continue
|
|
239
240
|
try:
|
|
240
241
|
msg = json.loads(line)
|
|
241
|
-
if isinstance(msg, dict):
|
|
242
|
-
raw_messages.append(cast(ConversationMessage, msg))
|
|
243
242
|
except ValueError:
|
|
244
243
|
continue
|
|
244
|
+
if isinstance(msg, dict):
|
|
245
|
+
raw_messages.append(cast(ConversationMessage, msg))
|
|
245
246
|
except FileNotFoundError:
|
|
246
247
|
pass
|
|
247
248
|
|
|
@@ -26,9 +26,11 @@ import typing
|
|
|
26
26
|
from base64 import b64encode
|
|
27
27
|
from collections import deque
|
|
28
28
|
from collections.abc import Callable, Mapping, Sequence
|
|
29
|
+
from contextlib import suppress
|
|
29
30
|
from dataclasses import dataclass
|
|
30
31
|
from difflib import SequenceMatcher, unified_diff
|
|
31
32
|
from pathlib import Path
|
|
33
|
+
from types import FunctionType
|
|
32
34
|
from typing import Any, TextIO, cast, overload
|
|
33
35
|
|
|
34
36
|
from griffe import Docstring, DocstringSectionKind, Parser
|
|
@@ -222,10 +224,8 @@ def _kill_proc_tree(proc: subprocess.Popen[str]) -> None:
|
|
|
222
224
|
else:
|
|
223
225
|
proc.kill()
|
|
224
226
|
except Exception:
|
|
225
|
-
|
|
227
|
+
with suppress(Exception):
|
|
226
228
|
proc.kill()
|
|
227
|
-
except Exception:
|
|
228
|
-
pass
|
|
229
229
|
|
|
230
230
|
|
|
231
231
|
# ---------------------------------------------------------------------------
|
|
@@ -235,7 +235,7 @@ def _kill_proc_tree(proc: subprocess.Popen[str]) -> None:
|
|
|
235
235
|
|
|
236
236
|
@overload
|
|
237
237
|
def tool(
|
|
238
|
-
function:
|
|
238
|
+
function: FunctionType,
|
|
239
239
|
*,
|
|
240
240
|
name: str | None = None,
|
|
241
241
|
description: str | None = None,
|
|
@@ -252,17 +252,17 @@ def tool(
|
|
|
252
252
|
description: str | None = None,
|
|
253
253
|
parameters: Mapping[str, str] | None = None,
|
|
254
254
|
streams_output: bool = False,
|
|
255
|
-
) -> Callable[[
|
|
255
|
+
) -> Callable[[FunctionType], ToolSpec]: ...
|
|
256
256
|
|
|
257
257
|
|
|
258
258
|
def tool(
|
|
259
|
-
function:
|
|
259
|
+
function: FunctionType | None = None,
|
|
260
260
|
*,
|
|
261
261
|
name: str | None = None,
|
|
262
262
|
description: str | None = None,
|
|
263
263
|
parameters: Mapping[str, str] | None = None,
|
|
264
264
|
streams_output: bool = False,
|
|
265
|
-
) -> ToolSpec | Callable[[
|
|
265
|
+
) -> ToolSpec | Callable[[FunctionType], ToolSpec]:
|
|
266
266
|
"""Wrap a sync or async Python function as a :class:`ToolSpec`."""
|
|
267
267
|
|
|
268
268
|
current_frame = inspect.currentframe()
|
|
@@ -271,7 +271,7 @@ def tool(
|
|
|
271
271
|
del current_frame
|
|
272
272
|
del caller_frame
|
|
273
273
|
|
|
274
|
-
def wrap(fn:
|
|
274
|
+
def wrap(fn: FunctionType) -> ToolSpec:
|
|
275
275
|
tool_name = name or fn.__name__
|
|
276
276
|
signature = inspect.signature(fn)
|
|
277
277
|
signature_parameters = list(signature.parameters.values())
|
|
@@ -421,7 +421,7 @@ def _coerce_tool_result(value: Any) -> ToolExecutionResult:
|
|
|
421
421
|
if isinstance(value, str):
|
|
422
422
|
return ToolExecutionResult(output=value)
|
|
423
423
|
try:
|
|
424
|
-
text = json.dumps(value, ensure_ascii=False)
|
|
424
|
+
text = json.dumps(value, ensure_ascii=False, default=str)
|
|
425
425
|
except TypeError:
|
|
426
426
|
text = str(value)
|
|
427
427
|
return ToolExecutionResult(output=text)
|
|
@@ -901,10 +901,8 @@ def bash_tool(ctx: ToolContext, command: str, timeout: int | None = None) -> Too
|
|
|
901
901
|
return ToolExecutionResult(output=f"error: {exc}", is_error=True)
|
|
902
902
|
finally:
|
|
903
903
|
if log_file is not None:
|
|
904
|
-
|
|
904
|
+
with suppress(Exception):
|
|
905
905
|
log_file.close()
|
|
906
|
-
except Exception:
|
|
907
|
-
pass
|
|
908
906
|
if proc is not None:
|
|
909
907
|
ctx.untrack_proc(proc)
|
|
910
908
|
if proc.poll() is None:
|
|
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
|