mycode-sdk 0.7.1__tar.gz → 0.7.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycode-sdk
3
- Version: 0.7.1
3
+ Version: 0.7.3
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mycode-sdk"
7
- version = "0.7.1"
7
+ version = "0.7.3"
8
8
  description = "Lightweight Python SDK for building AI agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -11,6 +11,7 @@ import asyncio
11
11
  import logging
12
12
  import os
13
13
  import tempfile
14
+ import time
14
15
  from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
15
16
  from dataclasses import dataclass, field
16
17
  from pathlib import Path
@@ -493,6 +494,8 @@ class Agent:
493
494
  return
494
495
 
495
496
  assistant_message: ConversationMessage | None = None
497
+ thinking_started_at: float | None = None
498
+ thinking_duration_ms: int | None = None
496
499
  request = ProviderRequest(
497
500
  provider=self.provider,
498
501
  model=self.model,
@@ -518,12 +521,17 @@ class Agent:
518
521
  if provider_event.type == "thinking_delta":
519
522
  delta_text = str(provider_event.data.get("text") or "")
520
523
  if delta_text:
524
+ if thinking_started_at is None:
525
+ thinking_started_at = time.monotonic()
521
526
  yield Event("reasoning", {"delta": delta_text})
522
527
  continue
523
528
 
524
529
  if provider_event.type == "text_delta":
525
530
  delta_text = str(provider_event.data.get("text") or "")
526
531
  if delta_text:
532
+ if thinking_started_at is not None and thinking_duration_ms is None:
533
+ thinking_duration_ms = max(0, int((time.monotonic() - thinking_started_at) * 1000))
534
+ yield Event("reasoning_done", {"duration_ms": thinking_duration_ms})
527
535
  yield Event("text", {"delta": delta_text})
528
536
  continue
529
537
 
@@ -533,6 +541,10 @@ class Agent:
533
541
  if provider_event.type != "message_done":
534
542
  continue
535
543
 
544
+ if thinking_started_at is not None and thinking_duration_ms is None:
545
+ thinking_duration_ms = max(0, int((time.monotonic() - thinking_started_at) * 1000))
546
+ yield Event("reasoning_done", {"duration_ms": thinking_duration_ms})
547
+
536
548
  message = provider_event.data.get("message")
537
549
  if isinstance(message, dict):
538
550
  assistant_message = message
@@ -549,6 +561,15 @@ class Agent:
549
561
  yield Event("error", {"message": "provider produced no assistant message"})
550
562
  return
551
563
 
564
+ if thinking_duration_ms is not None:
565
+ for block in reversed(assistant_message.get("content") or []):
566
+ if not isinstance(block, dict) or block.get("type") != "thinking":
567
+ continue
568
+ raw_meta = block.get("meta")
569
+ meta = cast(dict[str, Any], raw_meta) if isinstance(raw_meta, dict) else {}
570
+ block["meta"] = {**meta, "duration_ms": thinking_duration_ms}
571
+ break
572
+
552
573
  self.messages.append(assistant_message)
553
574
  await persist(assistant_message)
554
575
 
@@ -15,6 +15,8 @@ Metadata contract:
15
15
  `provider`, `model`, `provider_message_id`, `stop_reason`, `usage`
16
16
  - provider-specific assistant message extras live under `meta.native`
17
17
  - provider-specific block replay hints live under `block.meta.native`
18
+ - local display metadata, such as `block.meta.duration_ms`, is never sent
19
+ upstream; provider adapters must explicitly project only supported fields
18
20
  """
19
21
 
20
22
  from __future__ import annotations
@@ -205,7 +205,7 @@ def repair_messages_for_replay(
205
205
  block_type = raw_block.get("type")
206
206
  if block_type in {"text", "thinking"}:
207
207
  text = str(raw_block.get("text") or "")
208
- if text:
208
+ if text or get_native_meta(raw_block):
209
209
  content.append(dict(raw_block))
210
210
  continue
211
211
 
@@ -79,9 +79,10 @@ class OpenAIChatAdapter(ProviderAdapter):
79
79
 
80
80
  delta = choice.delta
81
81
  reasoning_delta, reasoning_meta_update = self._extract_reasoning_delta(delta)
82
+ if reasoning_meta_update:
83
+ thinking_native_meta.update(reasoning_meta_update)
82
84
  if reasoning_delta:
83
85
  thinking_parts.append(reasoning_delta)
84
- thinking_native_meta.update(reasoning_meta_update)
85
86
  yield ProviderStreamEvent("thinking_delta", {"text": reasoning_delta})
86
87
 
87
88
  if delta.content:
@@ -104,7 +105,7 @@ class OpenAIChatAdapter(ProviderAdapter):
104
105
  raise ValueError(str(exc)) from exc
105
106
 
106
107
  blocks = []
107
- if thinking_parts:
108
+ if thinking_parts or thinking_native_meta:
108
109
  blocks.append(
109
110
  thinking_block(
110
111
  "".join(thinking_parts),
@@ -286,32 +287,52 @@ class OpenAIChatAdapter(ProviderAdapter):
286
287
  reasoning_field = str(native_meta.get("reasoning_field") or "")
287
288
  if reasoning_field == "reasoning_details":
288
289
  return {"reasoning_details": native_meta.get("reasoning_details") or []}
290
+ if reasoning_field == "reasoning":
291
+ return {"reasoning": thinking_text or None}
292
+ if reasoning_field == "reasoning_content":
293
+ return {"reasoning_content": thinking_text or None}
289
294
  return {"reasoning_content": thinking_text} if thinking_text else {}
290
295
 
291
296
  def _extract_reasoning_delta(self, delta: Any) -> tuple[str, dict[str, Any]]:
292
297
  # Third-party providers surface reasoning through non-standard extras.
293
298
  # We check both the delta root and model_extra to cover both patterns.
294
- # Known fields: reasoning_content (Moonshot/MiniMax chat), reasoning_details (some others).
299
+ # Known fields: reasoning, reasoning_content, reasoning_details.
295
300
  for source in (delta, getattr(delta, "model_extra", None) or {}):
296
301
  if isinstance(source, dict):
302
+ has_reasoning = "reasoning" in source
303
+ reasoning = source.get("reasoning")
304
+ has_reasoning_content = "reasoning_content" in source
297
305
  reasoning_content = source.get("reasoning_content")
306
+ has_reasoning_details = "reasoning_details" in source
298
307
  reasoning_details = source.get("reasoning_details")
299
308
  else:
309
+ has_reasoning = hasattr(source, "reasoning")
310
+ reasoning = getattr(source, "reasoning", None)
311
+ has_reasoning_content = hasattr(source, "reasoning_content")
300
312
  reasoning_content = getattr(source, "reasoning_content", None)
313
+ has_reasoning_details = hasattr(source, "reasoning_details")
301
314
  reasoning_details = getattr(source, "reasoning_details", None)
302
315
 
303
- if isinstance(reasoning_content, str) and reasoning_content:
304
- return reasoning_content, {"reasoning_field": "reasoning_content"}
316
+ if has_reasoning:
317
+ return (
318
+ reasoning if isinstance(reasoning, str) else "",
319
+ {"reasoning_field": "reasoning"},
320
+ )
305
321
 
306
- if isinstance(reasoning_details, list) and reasoning_details:
322
+ if has_reasoning_content:
323
+ return (
324
+ reasoning_content if isinstance(reasoning_content, str) else "",
325
+ {"reasoning_field": "reasoning_content"},
326
+ )
327
+
328
+ if has_reasoning_details and isinstance(reasoning_details, list):
307
329
  reasoning_text = "".join(
308
330
  str(item.get("text") or "") for item in reasoning_details if isinstance(item, dict)
309
331
  )
310
- if reasoning_text:
311
- return reasoning_text, {
312
- "reasoning_field": "reasoning_details",
313
- "reasoning_details": reasoning_details,
314
- }
332
+ return reasoning_text, {
333
+ "reasoning_field": "reasoning_details",
334
+ "reasoning_details": reasoning_details,
335
+ }
315
336
 
316
337
  return "", {}
317
338
 
File without changes
File without changes
File without changes