mycode-sdk 0.7.3__tar.gz → 0.7.5__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.
@@ -86,3 +86,5 @@ CLAUDE.md
86
86
  GEMINI.md
87
87
  .gemini/
88
88
  .pi/
89
+ .agents/
90
+ skills-lock.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycode-sdk
3
- Version: 0.7.3
3
+ Version: 0.7.5
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.3"
7
+ version = "0.7.5"
8
8
  description = "Lightweight Python SDK for building AI agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -183,7 +183,7 @@ class Agent:
183
183
  supports_pdf_input=supports_pdf_input,
184
184
  )
185
185
  self.max_tokens: int = meta.max_output_tokens or 16_384
186
- self.context_window: int | None = meta.context_window or 128_000
186
+ self.context_window: int = meta.context_window or 128_000
187
187
  self.supports_reasoning: bool | None = meta.supports_reasoning
188
188
  self.supports_image_input: bool = bool(meta.supports_image_input)
189
189
  self.supports_pdf_input: bool = bool(meta.supports_pdf_input)
@@ -570,62 +570,83 @@ class Agent:
570
570
  block["meta"] = {**meta, "duration_ms": thinking_duration_ms}
571
571
  break
572
572
 
573
+ # Stamp context_window onto the persisted assistant message so
574
+ # rewinds and refreshed clients can render token-usage % without
575
+ # re-resolving model metadata.
576
+ meta = cast(dict[str, Any], assistant_message.setdefault("meta", {}))
577
+ meta["context_window"] = self.context_window
578
+
573
579
  self.messages.append(assistant_message)
574
580
  await persist(assistant_message)
575
581
 
576
- # Phase 2: if the assistant requested tools, execute them locally and
577
- # append one user-side tool_result message before continuing.
582
+ total_tokens = meta.get("total_tokens")
583
+ if total_tokens:
584
+ payload: dict[str, Any] = {
585
+ "total_tokens": total_tokens,
586
+ "model": meta.get("model") or self.model,
587
+ "provider": meta.get("provider") or self.provider,
588
+ "context_window": meta["context_window"],
589
+ }
590
+ yield Event("usage", payload)
591
+
578
592
  tool_calls = [
579
593
  block
580
594
  for block in assistant_message.get("content") or []
581
595
  if isinstance(block, dict) and block.get("type") == "tool_use"
582
596
  ]
583
- if not tool_calls:
584
- break
597
+ if tool_calls:
598
+ tool_results: list[dict[str, Any]] = []
599
+ for tool_call in tool_calls:
600
+ async for event in self._run_tool_call(tool_call):
601
+ yield event
602
+
603
+ if event.type != "tool_done":
604
+ continue
605
+
606
+ d = event.data
607
+ output = str(d.get("output") or "")
608
+ metadata = d.get("metadata") if isinstance(d.get("metadata"), dict) else None
609
+ content = d.get("content")
610
+ tool_results.append(
611
+ tool_result_block(
612
+ tool_use_id=str(d.get("tool_use_id") or ""),
613
+ output=output,
614
+ metadata=metadata,
615
+ is_error=bool(d.get("is_error")),
616
+ content=content if isinstance(content, list) else None,
617
+ )
618
+ )
585
619
 
586
- tool_results: list[dict[str, Any]] = []
587
- for tool_call in tool_calls:
588
- async for event in self._run_tool_call(tool_call):
589
- yield event
620
+ if self._cancel_event.is_set():
621
+ tool_result_message = build_message("user", tool_results)
622
+ self.messages.append(tool_result_message)
623
+ await persist(tool_result_message)
624
+ return
590
625
 
591
- if event.type != "tool_done":
592
- continue
626
+ tool_result_message = build_message("user", tool_results)
627
+ self.messages.append(tool_result_message)
628
+ await persist(tool_result_message)
593
629
 
594
- d = event.data
595
- output = str(d.get("output") or "")
596
- metadata = d.get("metadata") if isinstance(d.get("metadata"), dict) else None
597
- content = d.get("content")
598
- tool_results.append(
599
- tool_result_block(
600
- tool_use_id=str(d.get("tool_use_id") or ""),
601
- output=output,
602
- metadata=metadata,
603
- is_error=bool(d.get("is_error")),
604
- content=content if isinstance(content, list) else None,
605
- )
630
+ if self._cancel_event.is_set():
631
+ return
632
+ if should_compact(total_tokens, self.context_window, self.compact_threshold):
633
+ try:
634
+ async for event in self._compact(adapter, persist):
635
+ yield event
636
+ except (Exception, asyncio.CancelledError):
637
+ logger.warning(
638
+ "Context compaction failed, continuing without compaction",
639
+ exc_info=True,
606
640
  )
607
641
 
608
- if self._cancel_event.is_set():
609
- tool_result_message = build_message("user", tool_results)
610
- self.messages.append(tool_result_message)
611
- await persist(tool_result_message)
612
- return
613
-
614
- tool_result_message = build_message("user", tool_results)
615
- self.messages.append(tool_result_message)
616
- await persist(tool_result_message)
642
+ if not tool_calls:
643
+ break
617
644
 
618
645
  else:
619
646
  # while loop exhausted max_turns without breaking
620
647
  yield Event("error", {"message": "max_turns reached"})
621
648
  return
622
649
 
623
- # Turn completed normally (assistant stopped calling tools).
624
- # Check whether context compaction is needed.
625
- if not self._cancel_event.is_set():
626
- async for event in self._compact_if_needed(adapter, persist):
627
- yield event
628
-
629
650
  def run(
630
651
  self,
631
652
  user_input: str | ConversationMessage,
@@ -657,28 +678,6 @@ class Agent:
657
678
  # Context compaction
658
679
  # ------------------------------------------------------------------
659
680
 
660
- async def _compact_if_needed(
661
- self,
662
- adapter: ProviderAdapter,
663
- persist: PersistCallback,
664
- ) -> AsyncIterator[Event]:
665
- """Check token usage and run compaction if above threshold."""
666
-
667
- usage: dict[str, Any] | None = None
668
- for message in reversed(self.messages):
669
- if message.get("role") == "assistant":
670
- usage = (message.get("meta") or {}).get("usage")
671
- break
672
-
673
- if not should_compact(usage, self.context_window, self.compact_threshold):
674
- return
675
-
676
- try:
677
- async for event in self._compact(adapter, persist):
678
- yield event
679
- except (Exception, asyncio.CancelledError):
680
- logger.warning("Context compaction failed, continuing without compaction", exc_info=True)
681
-
682
681
  async def _compact(
683
682
  self,
684
683
  adapter: ProviderAdapter,
@@ -720,13 +719,13 @@ class Agent:
720
719
  logger.warning("Compaction produced empty summary")
721
720
  return
722
721
 
723
- summary_usage = (summary_message.get("meta") or {}).get("usage")
722
+ summary_total_tokens = (summary_message.get("meta") or {}).get("total_tokens")
724
723
  compact_event = build_compact_event(
725
724
  summary_text,
726
725
  provider=self.provider,
727
726
  model=self.model,
728
727
  compacted_count=compacted_count,
729
- usage=summary_usage,
728
+ total_tokens=summary_total_tokens,
730
729
  )
731
730
 
732
731
  # Persist the compact event (append-only — original messages stay in JSONL).
@@ -12,7 +12,8 @@ details.
12
12
  Metadata contract:
13
13
 
14
14
  - assistant message `meta` keeps normalized top-level fields only:
15
- `provider`, `model`, `provider_message_id`, `stop_reason`, `usage`
15
+ `provider`, `model`, `provider_message_id`, `stop_reason`, `total_tokens`,
16
+ `context_window` (see docs/sessions.md for `total_tokens` semantics)
16
17
  - provider-specific assistant message extras live under `meta.native`
17
18
  - provider-specific block replay hints live under `block.meta.native`
18
19
  - local display metadata, such as `block.meta.duration_ms`, is never sent
@@ -146,7 +147,7 @@ def assistant_message(
146
147
  model: str | None = None,
147
148
  provider_message_id: str | None = None,
148
149
  stop_reason: str | None = None,
149
- usage: Any = None,
150
+ total_tokens: int | None = None,
150
151
  native_meta: dict[str, Any] | None = None,
151
152
  ) -> ConversationMessage:
152
153
  """Build a normalized assistant message with shared metadata fields."""
@@ -160,8 +161,8 @@ def assistant_message(
160
161
  meta["provider_message_id"] = provider_message_id
161
162
  if stop_reason:
162
163
  meta["stop_reason"] = stop_reason
163
- if usage is not None:
164
- meta["usage"] = usage
164
+ if total_tokens is not None:
165
+ meta["total_tokens"] = total_tokens
165
166
  if native_meta:
166
167
  native = omit_none(native_meta)
167
168
  if native:
@@ -789,7 +789,7 @@
789
789
  },
790
790
  "gpt-5.5": {
791
791
  "context_window": 1050000,
792
- "max_output_tokens": 130000,
792
+ "max_output_tokens": 128000,
793
793
  "supports_image_input": true,
794
794
  "supports_pdf_input": true,
795
795
  "supports_reasoning": true
@@ -1181,6 +1181,13 @@
1181
1181
  "supports_pdf_input": true,
1182
1182
  "supports_reasoning": true
1183
1183
  },
1184
+ "google/gemini-3.1-flash-image-preview": {
1185
+ "context_window": 65536,
1186
+ "max_output_tokens": 65536,
1187
+ "supports_image_input": true,
1188
+ "supports_pdf_input": false,
1189
+ "supports_reasoning": true
1190
+ },
1184
1191
  "google/gemini-3.1-flash-lite-preview": {
1185
1192
  "context_window": 1048576,
1186
1193
  "max_output_tokens": 65536,
@@ -1743,7 +1750,7 @@
1743
1750
  },
1744
1751
  "openai/gpt-5.5": {
1745
1752
  "context_window": 1050000,
1746
- "max_output_tokens": 130000,
1753
+ "max_output_tokens": 128000,
1747
1754
  "supports_image_input": true,
1748
1755
  "supports_pdf_input": true,
1749
1756
  "supports_reasoning": true
@@ -1811,6 +1818,13 @@
1811
1818
  "supports_pdf_input": false,
1812
1819
  "supports_reasoning": true
1813
1820
  },
1821
+ "openrouter/pareto-code": {
1822
+ "context_window": 200000,
1823
+ "max_output_tokens": 200000,
1824
+ "supports_image_input": false,
1825
+ "supports_pdf_input": false,
1826
+ "supports_reasoning": true
1827
+ },
1814
1828
  "prime-intellect/intellect-3": {
1815
1829
  "context_window": 131072,
1816
1830
  "max_output_tokens": 8192,
@@ -2056,6 +2070,20 @@
2056
2070
  "supports_pdf_input": false,
2057
2071
  "supports_reasoning": true
2058
2072
  },
2073
+ "xiaomi/mimo-v2.5": {
2074
+ "context_window": 1048576,
2075
+ "max_output_tokens": 131072,
2076
+ "supports_image_input": false,
2077
+ "supports_pdf_input": false,
2078
+ "supports_reasoning": true
2079
+ },
2080
+ "xiaomi/mimo-v2.5-pro": {
2081
+ "context_window": 1048576,
2082
+ "max_output_tokens": 131072,
2083
+ "supports_image_input": true,
2084
+ "supports_pdf_input": true,
2085
+ "supports_reasoning": true
2086
+ },
2059
2087
  "z-ai/glm-4.5": {
2060
2088
  "context_window": 128000,
2061
2089
  "max_output_tokens": 96000,
@@ -219,13 +219,24 @@ class AnthropicLikeAdapter(ProviderAdapter):
219
219
  native_meta["stop_sequence"] = stop_sequence
220
220
  if service_tier := getattr(message, "service_tier", None):
221
221
  native_meta["service_tier"] = service_tier
222
+
223
+ # No `total_tokens` field — compute it from input + cache + output parts.
224
+ raw_usage = dump_model(getattr(message, "usage", None)) or {}
225
+ prompt_tokens = (
226
+ (raw_usage.get("input_tokens") or 0)
227
+ + (raw_usage.get("cache_creation_input_tokens") or 0)
228
+ + (raw_usage.get("cache_read_input_tokens") or 0)
229
+ )
230
+ output_tokens = raw_usage.get("output_tokens") or 0
231
+ total_tokens = prompt_tokens + output_tokens or None
232
+
222
233
  return assistant_message(
223
234
  blocks,
224
235
  provider=self.provider_id,
225
236
  model=getattr(message, "model", None),
226
237
  provider_message_id=getattr(message, "id", None),
227
238
  stop_reason=getattr(message, "stop_reason", None),
228
- usage=dump_model(getattr(message, "usage", None)),
239
+ total_tokens=total_tokens,
229
240
  native_meta=native_meta,
230
241
  )
231
242
 
@@ -92,6 +92,9 @@ class GoogleGeminiAdapter(ProviderAdapter):
92
92
  except Exception:
93
93
  pass
94
94
 
95
+ raw_usage = usage or {}
96
+ total_tokens = raw_usage.get("total_token_count") or None
97
+
95
98
  yield ProviderStreamEvent(
96
99
  "message_done",
97
100
  {
@@ -101,7 +104,7 @@ class GoogleGeminiAdapter(ProviderAdapter):
101
104
  model=response_model or request.model,
102
105
  provider_message_id=response_id,
103
106
  stop_reason=str(finish_reason) if finish_reason else None,
104
- usage=usage,
107
+ total_tokens=total_tokens,
105
108
  native_meta={"finish_message": str(finish_message)} if finish_message else None,
106
109
  )
107
110
  },
@@ -135,13 +135,16 @@ class OpenAIChatAdapter(ProviderAdapter):
135
135
  )
136
136
  )
137
137
 
138
+ raw_usage = dump_model(usage) or {}
139
+ total_tokens = raw_usage.get("total_tokens") or None
140
+
138
141
  final_message = assistant_message(
139
142
  blocks,
140
143
  provider=self.provider_id,
141
144
  model=response_model or request.model,
142
145
  provider_message_id=response_id,
143
146
  stop_reason=finish_reason,
144
- usage=dump_model(usage),
147
+ total_tokens=total_tokens,
145
148
  )
146
149
  yield ProviderStreamEvent("message_done", {"message": final_message})
147
150
 
@@ -30,7 +30,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
30
30
  label = "OpenAI Responses"
31
31
  default_base_url = "https://api.openai.com/v1"
32
32
  env_api_key_names = ("OPENAI_API_KEY",)
33
- default_models = ("gpt-5.4", "gpt-5.4-mini")
33
+ default_models = ("gpt-5.5", "gpt-5.4-mini")
34
34
  supports_reasoning_effort = True
35
35
 
36
36
  async def stream_turn(self, request: ProviderRequest) -> AsyncIterator[ProviderStreamEvent]:
@@ -361,12 +361,15 @@ class OpenAIResponsesAdapter(ProviderAdapter):
361
361
  )
362
362
  )
363
363
 
364
+ raw_usage = dump_model(getattr(response, "usage", None)) or {}
365
+ total_tokens = raw_usage.get("total_tokens") or None
366
+
364
367
  return assistant_message(
365
368
  blocks,
366
369
  provider=self.provider_id,
367
370
  model=getattr(response, "model", None),
368
371
  provider_message_id=getattr(response, "id", None),
369
372
  stop_reason=getattr(response, "status", None),
370
- usage=dump_model(getattr(response, "usage", None)),
373
+ total_tokens=total_tokens,
371
374
  native_meta={"output_items": dumped_output_items} if dumped_output_items else None,
372
375
  )
@@ -67,20 +67,20 @@ def _now() -> str:
67
67
 
68
68
 
69
69
  def should_compact(
70
- last_usage: dict[str, Any] | None,
70
+ last_total_tokens: int | None,
71
71
  context_window: int | None,
72
72
  threshold: float,
73
73
  ) -> bool:
74
- """Return True when the last response input tokens exceed the threshold."""
74
+ """True when the latest call's `total_tokens` `context_window × threshold`.
75
75
 
76
- if not last_usage or not context_window or threshold <= 0:
77
- return False
76
+ `total_tokens` already covers the next API call's prompt floor, so it is
77
+ the right input here. The `(1 - threshold)` headroom is reserved for the
78
+ compact LLM call itself (see docs/sessions.md).
79
+ """
78
80
 
79
- # Providers report prompt/input usage under slightly different field names.
80
- input_tokens = int(
81
- last_usage.get("input_tokens") or last_usage.get("prompt_tokens") or last_usage.get("prompt_token_count") or 0
82
- )
83
- return input_tokens >= context_window * threshold
81
+ if not last_total_tokens or not context_window or threshold <= 0:
82
+ return False
83
+ return last_total_tokens >= context_window * threshold
84
84
 
85
85
 
86
86
  def build_compact_event(
@@ -89,7 +89,7 @@ def build_compact_event(
89
89
  provider: str,
90
90
  model: str,
91
91
  compacted_count: int,
92
- usage: dict[str, Any] | None = None,
92
+ total_tokens: int | None = None,
93
93
  ) -> ConversationMessage:
94
94
  """Build the compact event stored in session JSONL."""
95
95
 
@@ -98,8 +98,8 @@ def build_compact_event(
98
98
  "model": model,
99
99
  "compacted_count": compacted_count,
100
100
  }
101
- if usage is not None:
102
- meta["usage"] = usage
101
+ if total_tokens is not None:
102
+ meta["total_tokens"] = total_tokens
103
103
  return build_message("compact", [text_block(summary_text)], meta=meta)
104
104
 
105
105
 
File without changes
File without changes