mycode-sdk 0.7.6__tar.gz → 0.8.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycode-sdk
3
- Version: 0.7.6
3
+ Version: 0.8.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mycode-sdk"
7
- version = "0.7.6"
7
+ version = "0.8.0"
8
8
  description = "Lightweight Python SDK for building AI agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -23,6 +23,7 @@ from mycode.messages import (
23
23
  ConversationMessage,
24
24
  build_message,
25
25
  flatten_message_text,
26
+ text_block,
26
27
  tool_result_block,
27
28
  user_text_message,
28
29
  )
@@ -30,10 +31,13 @@ from mycode.models import infer_provider_from_model, resolve_model_metadata
30
31
  from mycode.providers import get_provider_adapter
31
32
  from mycode.providers.base import ProviderAdapter, ProviderRequest, ProviderStreamEvent
32
33
  from mycode.session import (
34
+ COMPACT_ACK,
33
35
  COMPACT_SUMMARY_PROMPT,
36
+ CONTINUATION_FOOTER,
37
+ CONTINUATION_HEADER,
34
38
  DEFAULT_COMPACT_THRESHOLD,
39
+ TRANSCRIPT_HINT,
35
40
  SessionStore,
36
- apply_compact,
37
41
  build_compact_event,
38
42
  should_compact,
39
43
  )
@@ -500,7 +504,7 @@ class Agent:
500
504
  provider=self.provider,
501
505
  model=self.model,
502
506
  session_id=self.session_id,
503
- messages=self.messages,
507
+ messages=self._project_for_provider(self.messages),
504
508
  system=self.system,
505
509
  tools=self.tools.definitions,
506
510
  max_tokens=self.max_tokens,
@@ -631,11 +635,14 @@ class Agent:
631
635
  return
632
636
  if should_compact(total_tokens, self.context_window, self.compact_threshold):
633
637
  try:
634
- async for event in self._compact(adapter, persist, continue_now=bool(tool_calls)):
638
+ async for event in self._compact(adapter, persist):
635
639
  yield event
636
640
  except asyncio.CancelledError:
637
- raise
641
+ yield Event("error", {"message": "cancelled"})
642
+ return
638
643
  except Exception:
644
+ # Best-effort: transient failures retry next threshold check;
645
+ # persistent ones surface from phase 1 of the next turn.
639
646
  logger.warning(
640
647
  "Context compaction failed, continuing without compaction",
641
648
  exc_info=True,
@@ -680,19 +687,52 @@ class Agent:
680
687
  # Context compaction
681
688
  # ------------------------------------------------------------------
682
689
 
690
+ def _project_for_provider(
691
+ self,
692
+ messages: list[ConversationMessage],
693
+ ) -> list[ConversationMessage]:
694
+ """Replace pre-compact history with a summary continuation."""
695
+
696
+ last_compact = -1
697
+ for i, message in enumerate(messages):
698
+ if message.get("role") == "compact":
699
+ last_compact = i
700
+
701
+ if last_compact < 0:
702
+ return messages
703
+
704
+ summary_text = ""
705
+ for block in messages[last_compact].get("content") or []:
706
+ if isinstance(block, dict) and block.get("type") == "text":
707
+ summary_text = str(block.get("text") or "")
708
+ break
709
+
710
+ tail = [m for m in messages[last_compact + 1 :] if m.get("role") != "compact"]
711
+ # No tail or assistant-led tail = mid-loop; append a resume instruction
712
+ # and skip the ack. A user-led tail needs the ack to keep alternation.
713
+ continue_now = not tail or tail[0].get("role") == "assistant"
714
+
715
+ parts = [CONTINUATION_HEADER, summary_text]
716
+ if self._store and self.session_id:
717
+ parts.append(TRANSCRIPT_HINT.format(path=self._store.messages_path(self.session_id)))
718
+ if continue_now:
719
+ parts.append(CONTINUATION_FOOTER)
720
+
721
+ projected = [build_message("user", [text_block("\n\n".join(parts))])]
722
+ if not continue_now:
723
+ projected.append(build_message("assistant", [text_block(COMPACT_ACK)]))
724
+ projected.extend(tail)
725
+ return projected
726
+
683
727
  async def _compact(
684
728
  self,
685
729
  adapter: ProviderAdapter,
686
730
  persist: PersistCallback,
687
- *,
688
- continue_now: bool,
689
731
  ) -> AsyncIterator[Event]:
690
- """Generate a conversation summary and replace in-memory messages."""
691
-
692
- compacted_count = len(self.messages)
732
+ """Generate a conversation summary and append a compact marker."""
693
733
 
694
734
  # Ask the same provider for a summary — no tools, just text generation.
695
- compact_messages = list(self.messages) + [user_text_message(COMPACT_SUMMARY_PROMPT)]
735
+ compact_messages = self._project_for_provider(self.messages) + [user_text_message(COMPACT_SUMMARY_PROMPT)]
696
736
  request = ProviderRequest(
697
737
  provider=self.provider,
698
738
  model=self.model,
@@ -726,24 +766,11 @@ class Agent:
726
766
  summary_text,
727
767
  provider=self.provider,
728
768
  model=self.model,
729
- compacted_count=compacted_count,
730
769
  total_tokens=summary_total_tokens,
731
770
  )
732
771
 
733
772
  # Persist the compact event (append-only — original messages stay in JSONL).
734
773
  await persist(compact_event)
735
-
736
774
  self.messages.append(compact_event)
737
- self.messages = apply_compact(
738
- self.messages,
739
- transcript_path=str(self._store.messages_path(self.session_id)) if self._store else None,
740
- continue_now=continue_now,
741
- )
742
775
 
743
- yield Event(
744
- "compact",
745
- {
746
- "message": f"Context compacted ({compacted_count} messages → summary)",
747
- "compacted_count": compacted_count,
748
- },
749
- )
776
+ yield Event("compact", {})
@@ -24,7 +24,7 @@ from mycode.messages import ConversationMessage, build_message, flatten_message_
24
24
  # Session format and compacting defaults
25
25
  # ---------------------------------------------------------------------
26
26
 
27
- MESSAGE_FORMAT_VERSION = 6
27
+ MESSAGE_FORMAT_VERSION = 7
28
28
  DEFAULT_COMPACT_THRESHOLD = 0.8
29
29
  DEFAULT_SESSION_TITLE = "New chat"
30
30
 
@@ -60,13 +60,13 @@ other identifiers verbatim — never paraphrase them.
60
60
  - Keep it concise but complete.\
61
61
  """
62
62
 
63
- _CONTINUATION_HEADER = "This session is being continued from a previous conversation that was compacted to fit the context window. The summary below covers the earlier portion of the conversation."
63
+ CONTINUATION_HEADER = "This session is being continued from a previous conversation that was compacted to fit the context window. The summary below covers the earlier portion of the conversation."
64
64
 
65
- _TRANSCRIPT_HINT = "For verbatim details not captured in this summary (exact code snippets, error messages, or earlier output), read the original conversation log at: {path}"
65
+ TRANSCRIPT_HINT = "For verbatim details not captured in this summary (exact code snippets, error messages, or earlier output), read the original conversation log at: {path}"
66
66
 
67
- _CONTINUATION_FOOTER = 'Resume directly from where the work left off. Do not acknowledge this summary, do not recap, and do not preface with "I\'ll continue" or similar.'
67
+ CONTINUATION_FOOTER = 'Resume directly from where the work left off. Do not acknowledge this summary, do not recap, and do not preface with "I\'ll continue" or similar.'
68
68
 
69
- _COMPACT_ACK = "Acknowledged."
69
+ COMPACT_ACK = "Acknowledged."
70
70
 
71
71
 
72
72
  # ---------------------------------------------------------------------
@@ -100,7 +100,6 @@ def build_compact_event(
100
100
  *,
101
101
  provider: str,
102
102
  model: str,
103
- compacted_count: int,
104
103
  total_tokens: int | None = None,
105
104
  ) -> ConversationMessage:
106
105
  """Build the compact event stored in session JSONL."""
@@ -108,60 +107,12 @@ def build_compact_event(
108
107
  meta: dict[str, Any] = {
109
108
  "provider": provider,
110
109
  "model": model,
111
- "compacted_count": compacted_count,
112
110
  }
113
111
  if total_tokens is not None:
114
112
  meta["total_tokens"] = total_tokens
115
113
  return build_message("compact", [text_block(summary_text)], meta=meta)
116
114
 
117
115
 
118
- def apply_compact(
119
- messages: list[ConversationMessage],
120
- *,
121
- transcript_path: str | None = None,
122
- continue_now: bool | None = None,
123
- ) -> list[ConversationMessage]:
124
- """Replace the latest compact event with a synthetic summary view.
125
-
126
- ``continue_now`` omits the ack and leaves a user instruction last so the
127
- agent loop can immediately request the next assistant response.
128
- """
129
-
130
- # Only the newest compact event matters. Older history before it is no
131
- # longer visible once the summary replaces that earlier conversation.
132
- last_compact_index: int | None = None
133
- for index, message in enumerate(messages):
134
- if message.get("role") == "compact":
135
- last_compact_index = index
136
-
137
- if last_compact_index is None:
138
- return messages
139
-
140
- summary_text = ""
141
- for block in messages[last_compact_index].get("content") or []:
142
- if isinstance(block, dict) and block.get("type") == "text":
143
- summary_text = str(block.get("text") or "")
144
- break
145
-
146
- tail = messages[last_compact_index + 1 :]
147
- if continue_now is None:
148
- # During live tool-loop compaction the next persisted message is the
149
- # assistant continuation. Waiting compaction has no tail yet.
150
- continue_now = bool(tail and tail[0].get("role") == "assistant")
151
-
152
- parts = [_CONTINUATION_HEADER, summary_text]
153
- if transcript_path:
154
- parts.append(_TRANSCRIPT_HINT.format(path=transcript_path))
155
- if continue_now:
156
- parts.append(_CONTINUATION_FOOTER)
157
-
158
- result = [build_message("user", [text_block("\n\n".join(parts))], meta={"synthetic": True})]
159
- if not continue_now:
160
- result.append(build_message("assistant", [text_block(_COMPACT_ACK)], meta={"synthetic": True}))
161
- result.extend(tail)
162
- return result
163
-
164
-
165
116
  def build_rewind_event(rewind_to: int) -> ConversationMessage:
166
117
  """Build a rewind marker to append to session JSONL."""
167
118
 
@@ -341,16 +292,11 @@ class SessionStore:
341
292
  except FileNotFoundError:
342
293
  pass
343
294
 
344
- # Replay order defines the visible conversation state.
345
- # 1) compact rewrites older history into one summary view
346
- # 2) rewind truncates that visible list by message index
295
+ # Visible state = raw JSONL minus rewound tails. `compact` markers
296
+ # stay inline; the agent substitutes them when calling the provider.
347
297
  # Orphan tool_use blocks (e.g. left open by a server crash) are
348
298
  # closed by the provider adapter at replay time, not here.
349
- visible_messages = apply_compact(
350
- raw_messages,
351
- transcript_path=str(self.messages_path(session_id)),
352
- )
353
- visible_messages = apply_rewind(visible_messages)
299
+ visible_messages = apply_rewind(raw_messages)
354
300
 
355
301
  return {"session": self._summary(session_id, meta), "messages": visible_messages}
356
302
 
File without changes
File without changes
File without changes