mycode-sdk 0.8.0__tar.gz → 0.8.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.8.0 → mycode_sdk-0.8.1}/PKG-INFO +1 -1
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/pyproject.toml +1 -1
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/agent.py +30 -75
- mycode_sdk-0.8.1/src/mycode/compact.py +120 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/providers/anthropic_like.py +4 -4
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/providers/openai_chat.py +16 -34
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/providers/openai_responses.py +1 -1
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/session.py +4 -80
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/utils.py +4 -6
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/.gitignore +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/LICENSE +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/README.md +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/__init__.py +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/hooks.py +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/messages.py +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/models.py +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/models_catalog.json +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/providers/base.py +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/providers/gemini.py +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/py.typed +0 -0
- {mycode_sdk-0.8.0 → mycode_sdk-0.8.1}/src/mycode/tools.py +0 -0
|
@@ -18,29 +18,25 @@ from pathlib import Path
|
|
|
18
18
|
from typing import Any, cast
|
|
19
19
|
from uuid import uuid4
|
|
20
20
|
|
|
21
|
+
from mycode.compact import (
|
|
22
|
+
COMPACT_SUMMARY_PROMPT,
|
|
23
|
+
DEFAULT_COMPACT_THRESHOLD,
|
|
24
|
+
apply_compact_replay,
|
|
25
|
+
build_compact_event,
|
|
26
|
+
should_compact,
|
|
27
|
+
)
|
|
21
28
|
from mycode.hooks import Hooks, ToolHookContext
|
|
22
29
|
from mycode.messages import (
|
|
23
30
|
ConversationMessage,
|
|
24
31
|
build_message,
|
|
25
32
|
flatten_message_text,
|
|
26
|
-
text_block,
|
|
27
33
|
tool_result_block,
|
|
28
34
|
user_text_message,
|
|
29
35
|
)
|
|
30
36
|
from mycode.models import infer_provider_from_model, resolve_model_metadata
|
|
31
37
|
from mycode.providers import get_provider_adapter
|
|
32
38
|
from mycode.providers.base import ProviderAdapter, ProviderRequest, ProviderStreamEvent
|
|
33
|
-
from mycode.session import
|
|
34
|
-
COMPACT_ACK,
|
|
35
|
-
COMPACT_SUMMARY_PROMPT,
|
|
36
|
-
CONTINUATION_FOOTER,
|
|
37
|
-
CONTINUATION_HEADER,
|
|
38
|
-
DEFAULT_COMPACT_THRESHOLD,
|
|
39
|
-
TRANSCRIPT_HINT,
|
|
40
|
-
SessionStore,
|
|
41
|
-
build_compact_event,
|
|
42
|
-
should_compact,
|
|
43
|
-
)
|
|
39
|
+
from mycode.session import SessionStore
|
|
44
40
|
from mycode.tools import ToolContext, ToolExecutionResult, ToolExecutor, ToolSpec
|
|
45
41
|
|
|
46
42
|
logger = logging.getLogger(__name__)
|
|
@@ -108,6 +104,7 @@ class Agent:
|
|
|
108
104
|
self.session_dir = session_dir
|
|
109
105
|
self.session_id = (session_id or "").strip() or uuid4().hex
|
|
110
106
|
self._store: SessionStore | None = SessionStore(data_dir=session_dir) if session_dir is not None else None
|
|
107
|
+
self.transcript_path: str | None = str(self._store.messages_path(self.session_id)) if self._store else None
|
|
111
108
|
|
|
112
109
|
self.api_key = api_key
|
|
113
110
|
self.api_base = api_base
|
|
@@ -455,32 +452,29 @@ class Agent:
|
|
|
455
452
|
await self._store.append_message(self.session_id, message)
|
|
456
453
|
|
|
457
454
|
self._cancel_event.clear()
|
|
458
|
-
supports_image_input = self.supports_image_input
|
|
459
|
-
supports_pdf_input = self.supports_pdf_input
|
|
460
|
-
self.tool_ctx.supports_image_input = supports_image_input
|
|
461
455
|
|
|
462
456
|
if isinstance(user_input, str):
|
|
463
|
-
user_message = user_text_message(user_input)
|
|
457
|
+
user_message: ConversationMessage = user_text_message(user_input)
|
|
464
458
|
else:
|
|
465
|
-
|
|
466
|
-
"
|
|
459
|
+
if (user_input.get("role") or "user") != "user":
|
|
460
|
+
yield Event("error", {"message": "user input must be a user message"})
|
|
461
|
+
return
|
|
462
|
+
user_message = {
|
|
463
|
+
"role": "user",
|
|
467
464
|
"content": [dict(b) for b in user_input.get("content") or [] if isinstance(b, dict)],
|
|
468
465
|
}
|
|
469
466
|
raw_meta = user_input.get("meta")
|
|
470
467
|
if isinstance(raw_meta, dict):
|
|
471
468
|
user_message["meta"] = {str(k): v for k, v in raw_meta.items()}
|
|
472
469
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if not supports_image_input and any(
|
|
478
|
-
isinstance(block, dict) and block.get("type") == "image" for block in user_message.get("content") or []
|
|
470
|
+
content_blocks = user_message.get("content") or []
|
|
471
|
+
if not self.supports_image_input and any(
|
|
472
|
+
isinstance(block, dict) and block.get("type") == "image" for block in content_blocks
|
|
479
473
|
):
|
|
480
474
|
yield Event("error", {"message": "current model does not support image input"})
|
|
481
475
|
return
|
|
482
|
-
if not supports_pdf_input and any(
|
|
483
|
-
isinstance(block, dict) and block.get("type") == "document" for block in
|
|
476
|
+
if not self.supports_pdf_input and any(
|
|
477
|
+
isinstance(block, dict) and block.get("type") == "document" for block in content_blocks
|
|
484
478
|
):
|
|
485
479
|
yield Event("error", {"message": "current model does not support PDF input"})
|
|
486
480
|
return
|
|
@@ -504,15 +498,15 @@ class Agent:
|
|
|
504
498
|
provider=self.provider,
|
|
505
499
|
model=self.model,
|
|
506
500
|
session_id=self.session_id,
|
|
507
|
-
messages=self.
|
|
501
|
+
messages=apply_compact_replay(self.messages, transcript_path=self.transcript_path),
|
|
508
502
|
system=self.system,
|
|
509
503
|
tools=self.tools.definitions,
|
|
510
504
|
max_tokens=self.max_tokens,
|
|
511
505
|
api_key=self.api_key,
|
|
512
506
|
api_base=self.api_base,
|
|
513
507
|
reasoning_effort=self.reasoning_effort,
|
|
514
|
-
supports_image_input=supports_image_input,
|
|
515
|
-
supports_pdf_input=supports_pdf_input,
|
|
508
|
+
supports_image_input=self.supports_image_input,
|
|
509
|
+
supports_pdf_input=self.supports_pdf_input,
|
|
516
510
|
)
|
|
517
511
|
|
|
518
512
|
try:
|
|
@@ -635,8 +629,8 @@ class Agent:
|
|
|
635
629
|
return
|
|
636
630
|
if should_compact(total_tokens, self.context_window, self.compact_threshold):
|
|
637
631
|
try:
|
|
638
|
-
|
|
639
|
-
|
|
632
|
+
await self._compact(adapter, persist)
|
|
633
|
+
yield Event("compact", {})
|
|
640
634
|
except asyncio.CancelledError:
|
|
641
635
|
yield Event("error", {"message": "cancelled"})
|
|
642
636
|
return
|
|
@@ -687,52 +681,16 @@ class Agent:
|
|
|
687
681
|
# Context compaction
|
|
688
682
|
# ------------------------------------------------------------------
|
|
689
683
|
|
|
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
|
-
|
|
727
684
|
async def _compact(
|
|
728
685
|
self,
|
|
729
686
|
adapter: ProviderAdapter,
|
|
730
687
|
persist: PersistCallback,
|
|
731
|
-
) ->
|
|
732
|
-
"""
|
|
688
|
+
) -> None:
|
|
689
|
+
"""Ask the provider for a summary, persist the compact event, append it."""
|
|
733
690
|
|
|
734
|
-
|
|
735
|
-
|
|
691
|
+
compact_messages = apply_compact_replay(self.messages, transcript_path=self.transcript_path) + [
|
|
692
|
+
user_text_message(COMPACT_SUMMARY_PROMPT)
|
|
693
|
+
]
|
|
736
694
|
request = ProviderRequest(
|
|
737
695
|
provider=self.provider,
|
|
738
696
|
model=self.model,
|
|
@@ -769,8 +727,5 @@ class Agent:
|
|
|
769
727
|
total_tokens=summary_total_tokens,
|
|
770
728
|
)
|
|
771
729
|
|
|
772
|
-
# Persist the compact event (append-only — original messages stay in JSONL).
|
|
773
730
|
await persist(compact_event)
|
|
774
731
|
self.messages.append(compact_event)
|
|
775
|
-
|
|
776
|
-
yield Event("compact", {})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Context compaction: summarize past history when nearing the context window."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mycode.messages import ConversationMessage, build_message, text_block
|
|
8
|
+
|
|
9
|
+
DEFAULT_COMPACT_THRESHOLD = 0.8
|
|
10
|
+
|
|
11
|
+
COMPACT_SUMMARY_PROMPT = """\
|
|
12
|
+
Summarize this conversation to create a continuation document. \
|
|
13
|
+
This summary will replace the full conversation history, so it must \
|
|
14
|
+
capture everything needed to continue the work seamlessly.
|
|
15
|
+
|
|
16
|
+
Include:
|
|
17
|
+
|
|
18
|
+
1. **Task and Intent**: Describe the user's overall goal — what is being \
|
|
19
|
+
built, fixed, or investigated, and why.
|
|
20
|
+
2. **Decisions and Constraints**: List the decisions made, constraints \
|
|
21
|
+
discovered, and approaches chosen or rejected, with the reasoning behind \
|
|
22
|
+
each.
|
|
23
|
+
3. **User Requests**: Every distinct request or instruction the user gave, \
|
|
24
|
+
in chronological order. Preserve the user's original wording for ambiguous \
|
|
25
|
+
or nuanced requests.
|
|
26
|
+
4. **Files and Changes**: Enumerate every file read, modified, or created \
|
|
27
|
+
— paths, what changed, and any code snippets the next turn will need to \
|
|
28
|
+
reason about, quoted verbatim.
|
|
29
|
+
5. **Errors and Fixes**: List errors encountered with the original message \
|
|
30
|
+
verbatim, the cause if known, and the resolution — or that it remains open.
|
|
31
|
+
6. **Current State**: What is verified working, what is known broken, what \
|
|
32
|
+
is in progress.
|
|
33
|
+
7. **Next Step**: The next step to take, with a direct quote from the most \
|
|
34
|
+
recent conversation showing where the work left off.
|
|
35
|
+
|
|
36
|
+
Rules:
|
|
37
|
+
- Be specific: reproduce file paths, function names, error messages, and \
|
|
38
|
+
other identifiers verbatim — never paraphrase them.
|
|
39
|
+
- Do not add suggestions or opinions — only summarize what happened.
|
|
40
|
+
- Keep it concise but complete.\
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
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."
|
|
44
|
+
|
|
45
|
+
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}"
|
|
46
|
+
|
|
47
|
+
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.'
|
|
48
|
+
|
|
49
|
+
COMPACT_ACK = "Acknowledged."
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def should_compact(
|
|
53
|
+
last_total_tokens: int | None,
|
|
54
|
+
context_window: int | None,
|
|
55
|
+
threshold: float,
|
|
56
|
+
) -> bool:
|
|
57
|
+
"""True when ``last_total_tokens`` ≥ ``context_window × threshold``.
|
|
58
|
+
|
|
59
|
+
The ``(1 - threshold)`` headroom is reserved for the compact LLM call
|
|
60
|
+
itself (see docs/sessions.md).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if not last_total_tokens or not context_window or threshold <= 0:
|
|
64
|
+
return False
|
|
65
|
+
return last_total_tokens >= context_window * threshold
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def build_compact_event(
|
|
69
|
+
summary_text: str,
|
|
70
|
+
*,
|
|
71
|
+
provider: str,
|
|
72
|
+
model: str,
|
|
73
|
+
total_tokens: int | None = None,
|
|
74
|
+
) -> ConversationMessage:
|
|
75
|
+
meta: dict[str, Any] = {"provider": provider, "model": model}
|
|
76
|
+
if total_tokens is not None:
|
|
77
|
+
meta["total_tokens"] = total_tokens
|
|
78
|
+
return build_message("compact", [text_block(summary_text)], meta=meta)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def apply_compact_replay(
|
|
82
|
+
messages: list[ConversationMessage],
|
|
83
|
+
*,
|
|
84
|
+
transcript_path: str | None = None,
|
|
85
|
+
) -> list[ConversationMessage]:
|
|
86
|
+
"""Replace pre-compact history with a summary continuation.
|
|
87
|
+
|
|
88
|
+
Returns ``messages`` unchanged when no compact event is present.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
last_compact = -1
|
|
92
|
+
for i, message in enumerate(messages):
|
|
93
|
+
if message.get("role") == "compact":
|
|
94
|
+
last_compact = i
|
|
95
|
+
|
|
96
|
+
if last_compact < 0:
|
|
97
|
+
return messages
|
|
98
|
+
|
|
99
|
+
summary_text = ""
|
|
100
|
+
for block in messages[last_compact].get("content") or []:
|
|
101
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
102
|
+
summary_text = str(block.get("text") or "")
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
tail = [m for m in messages[last_compact + 1 :] if m.get("role") != "compact"]
|
|
106
|
+
# No tail or assistant-led tail = mid-loop; resume directly. A user-led
|
|
107
|
+
# tail needs an "Acknowledged." assistant turn to keep role alternation.
|
|
108
|
+
continue_now = not tail or tail[0].get("role") == "assistant"
|
|
109
|
+
|
|
110
|
+
parts = [CONTINUATION_HEADER, summary_text]
|
|
111
|
+
if transcript_path:
|
|
112
|
+
parts.append(TRANSCRIPT_HINT.format(path=transcript_path))
|
|
113
|
+
if continue_now:
|
|
114
|
+
parts.append(CONTINUATION_FOOTER)
|
|
115
|
+
|
|
116
|
+
projected = [build_message("user", [text_block("\n\n".join(parts))])]
|
|
117
|
+
if not continue_now:
|
|
118
|
+
projected.append(build_message("assistant", [text_block(COMPACT_ACK)]))
|
|
119
|
+
projected.extend(tail)
|
|
120
|
+
return projected
|
|
@@ -214,11 +214,11 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
214
214
|
)
|
|
215
215
|
continue
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
message_native_meta: dict[str, Any] = {}
|
|
218
218
|
if stop_sequence := getattr(message, "stop_sequence", None):
|
|
219
|
-
|
|
219
|
+
message_native_meta["stop_sequence"] = stop_sequence
|
|
220
220
|
if service_tier := getattr(message, "service_tier", None):
|
|
221
|
-
|
|
221
|
+
message_native_meta["service_tier"] = service_tier
|
|
222
222
|
|
|
223
223
|
# No `total_tokens` field — compute it from input + cache + output parts.
|
|
224
224
|
raw_usage = dump_model(getattr(message, "usage", None)) or {}
|
|
@@ -237,7 +237,7 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
237
237
|
provider_message_id=getattr(message, "id", None),
|
|
238
238
|
stop_reason=getattr(message, "stop_reason", None),
|
|
239
239
|
total_tokens=total_tokens,
|
|
240
|
-
native_meta=
|
|
240
|
+
native_meta=message_native_meta,
|
|
241
241
|
)
|
|
242
242
|
|
|
243
243
|
def _serialize_tool(self, tool: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -119,7 +119,7 @@ class OpenAIChatAdapter(ProviderAdapter):
|
|
|
119
119
|
state = tool_calls[index]
|
|
120
120
|
raw_arguments = state.arguments_text
|
|
121
121
|
parsed_arguments = parse_tool_arguments(raw_arguments)
|
|
122
|
-
if
|
|
122
|
+
if parsed_arguments is None:
|
|
123
123
|
tool_input = {}
|
|
124
124
|
meta = {"native": {"raw_arguments": raw_arguments}}
|
|
125
125
|
else:
|
|
@@ -301,41 +301,23 @@ class OpenAIChatAdapter(ProviderAdapter):
|
|
|
301
301
|
# We check both the delta root and model_extra to cover both patterns.
|
|
302
302
|
# Known fields: reasoning, reasoning_content, reasoning_details.
|
|
303
303
|
for source in (delta, getattr(delta, "model_extra", None) or {}):
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
reasoning = getattr(source, "reasoning", None)
|
|
314
|
-
has_reasoning_content = hasattr(source, "reasoning_content")
|
|
315
|
-
reasoning_content = getattr(source, "reasoning_content", None)
|
|
316
|
-
has_reasoning_details = hasattr(source, "reasoning_details")
|
|
317
|
-
reasoning_details = getattr(source, "reasoning_details", None)
|
|
318
|
-
|
|
319
|
-
if has_reasoning:
|
|
320
|
-
return (
|
|
321
|
-
reasoning if isinstance(reasoning, str) else "",
|
|
322
|
-
{"reasoning_field": "reasoning"},
|
|
323
|
-
)
|
|
304
|
+
for field in ("reasoning", "reasoning_content", "reasoning_details"):
|
|
305
|
+
if isinstance(source, dict):
|
|
306
|
+
if field not in source:
|
|
307
|
+
continue
|
|
308
|
+
value = source[field]
|
|
309
|
+
elif hasattr(source, field):
|
|
310
|
+
value = getattr(source, field, None)
|
|
311
|
+
else:
|
|
312
|
+
continue
|
|
324
313
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
314
|
+
if field == "reasoning_details":
|
|
315
|
+
if not isinstance(value, list):
|
|
316
|
+
continue
|
|
317
|
+
text = "".join(str(item.get("text") or "") for item in value if isinstance(item, dict))
|
|
318
|
+
return text, {"reasoning_field": "reasoning_details", "reasoning_details": value}
|
|
330
319
|
|
|
331
|
-
|
|
332
|
-
reasoning_text = "".join(
|
|
333
|
-
str(item.get("text") or "") for item in reasoning_details if isinstance(item, dict)
|
|
334
|
-
)
|
|
335
|
-
return reasoning_text, {
|
|
336
|
-
"reasoning_field": "reasoning_details",
|
|
337
|
-
"reasoning_details": reasoning_details,
|
|
338
|
-
}
|
|
320
|
+
return (value if isinstance(value, str) else "", {"reasoning_field": field})
|
|
339
321
|
|
|
340
322
|
return "", {}
|
|
341
323
|
|
|
@@ -339,7 +339,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
|
|
|
339
339
|
if item_type == "function_call":
|
|
340
340
|
raw_arguments = getattr(item, "arguments", "") or ""
|
|
341
341
|
parsed_arguments = parse_tool_arguments(raw_arguments)
|
|
342
|
-
if
|
|
342
|
+
if parsed_arguments is None:
|
|
343
343
|
tool_input = {}
|
|
344
344
|
raw_args_entry: dict[str, Any] = {"raw_arguments": raw_arguments}
|
|
345
345
|
else:
|
|
@@ -16,61 +16,20 @@ import shutil
|
|
|
16
16
|
from dataclasses import asdict, dataclass
|
|
17
17
|
from datetime import UTC, datetime
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import
|
|
19
|
+
from typing import TypedDict, cast
|
|
20
20
|
|
|
21
|
-
from mycode.messages import ConversationMessage,
|
|
21
|
+
from mycode.messages import ConversationMessage, flatten_message_text
|
|
22
22
|
|
|
23
23
|
# ---------------------------------------------------------------------
|
|
24
|
-
# Session format
|
|
24
|
+
# Session format defaults
|
|
25
25
|
# ---------------------------------------------------------------------
|
|
26
26
|
|
|
27
27
|
MESSAGE_FORMAT_VERSION = 7
|
|
28
|
-
DEFAULT_COMPACT_THRESHOLD = 0.8
|
|
29
28
|
DEFAULT_SESSION_TITLE = "New chat"
|
|
30
29
|
|
|
31
|
-
COMPACT_SUMMARY_PROMPT = """\
|
|
32
|
-
Summarize this conversation to create a continuation document. \
|
|
33
|
-
This summary will replace the full conversation history, so it must \
|
|
34
|
-
capture everything needed to continue the work seamlessly.
|
|
35
|
-
|
|
36
|
-
Include:
|
|
37
|
-
|
|
38
|
-
1. **Task and Intent**: Describe the user's overall goal — what is being \
|
|
39
|
-
built, fixed, or investigated, and why.
|
|
40
|
-
2. **Decisions and Constraints**: List the decisions made, constraints \
|
|
41
|
-
discovered, and approaches chosen or rejected, with the reasoning behind \
|
|
42
|
-
each.
|
|
43
|
-
3. **User Requests**: Every distinct request or instruction the user gave, \
|
|
44
|
-
in chronological order. Preserve the user's original wording for ambiguous \
|
|
45
|
-
or nuanced requests.
|
|
46
|
-
4. **Files and Changes**: Enumerate every file read, modified, or created \
|
|
47
|
-
— paths, what changed, and any code snippets the next turn will need to \
|
|
48
|
-
reason about, quoted verbatim.
|
|
49
|
-
5. **Errors and Fixes**: List errors encountered with the original message \
|
|
50
|
-
verbatim, the cause if known, and the resolution — or that it remains open.
|
|
51
|
-
6. **Current State**: What is verified working, what is known broken, what \
|
|
52
|
-
is in progress.
|
|
53
|
-
7. **Next Step**: The next step to take, with a direct quote from the most \
|
|
54
|
-
recent conversation showing where the work left off.
|
|
55
|
-
|
|
56
|
-
Rules:
|
|
57
|
-
- Be specific: reproduce file paths, function names, error messages, and \
|
|
58
|
-
other identifiers verbatim — never paraphrase them.
|
|
59
|
-
- Do not add suggestions or opinions — only summarize what happened.
|
|
60
|
-
- Keep it concise but complete.\
|
|
61
|
-
"""
|
|
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."
|
|
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}"
|
|
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.'
|
|
68
|
-
|
|
69
|
-
COMPACT_ACK = "Acknowledged."
|
|
70
|
-
|
|
71
30
|
|
|
72
31
|
# ---------------------------------------------------------------------
|
|
73
|
-
#
|
|
32
|
+
# Rewind session events
|
|
74
33
|
# ---------------------------------------------------------------------
|
|
75
34
|
|
|
76
35
|
|
|
@@ -78,41 +37,6 @@ def _now() -> str:
|
|
|
78
37
|
return datetime.now(UTC).isoformat()
|
|
79
38
|
|
|
80
39
|
|
|
81
|
-
def should_compact(
|
|
82
|
-
last_total_tokens: int | None,
|
|
83
|
-
context_window: int | None,
|
|
84
|
-
threshold: float,
|
|
85
|
-
) -> bool:
|
|
86
|
-
"""True when the latest call's `total_tokens` ≥ `context_window × threshold`.
|
|
87
|
-
|
|
88
|
-
`total_tokens` already covers the next API call's prompt floor, so it is
|
|
89
|
-
the right input here. The `(1 - threshold)` headroom is reserved for the
|
|
90
|
-
compact LLM call itself (see docs/sessions.md).
|
|
91
|
-
"""
|
|
92
|
-
|
|
93
|
-
if not last_total_tokens or not context_window or threshold <= 0:
|
|
94
|
-
return False
|
|
95
|
-
return last_total_tokens >= context_window * threshold
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def build_compact_event(
|
|
99
|
-
summary_text: str,
|
|
100
|
-
*,
|
|
101
|
-
provider: str,
|
|
102
|
-
model: str,
|
|
103
|
-
total_tokens: int | None = None,
|
|
104
|
-
) -> ConversationMessage:
|
|
105
|
-
"""Build the compact event stored in session JSONL."""
|
|
106
|
-
|
|
107
|
-
meta: dict[str, Any] = {
|
|
108
|
-
"provider": provider,
|
|
109
|
-
"model": model,
|
|
110
|
-
}
|
|
111
|
-
if total_tokens is not None:
|
|
112
|
-
meta["total_tokens"] = total_tokens
|
|
113
|
-
return build_message("compact", [text_block(summary_text)], meta=meta)
|
|
114
|
-
|
|
115
|
-
|
|
116
40
|
def build_rewind_event(rewind_to: int) -> ConversationMessage:
|
|
117
41
|
"""Build a rewind marker to append to session JSONL."""
|
|
118
42
|
|
|
@@ -21,10 +21,10 @@ def omit_none(d: dict[str, Any]) -> dict[str, Any]:
|
|
|
21
21
|
return {k: v for k, v in d.items() if v is not None}
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def parse_tool_arguments(raw: str | None) -> dict[str, Any] |
|
|
24
|
+
def parse_tool_arguments(raw: str | None) -> dict[str, Any] | None:
|
|
25
25
|
"""Parse a JSON tool-arguments string.
|
|
26
26
|
|
|
27
|
-
Returns the parsed dict, or
|
|
27
|
+
Returns the parsed dict, or None when the input is missing/invalid/non-object.
|
|
28
28
|
Empty / None input is treated as an empty argument set.
|
|
29
29
|
"""
|
|
30
30
|
if not raw or not raw.strip():
|
|
@@ -32,7 +32,5 @@ def parse_tool_arguments(raw: str | None) -> dict[str, Any] | str:
|
|
|
32
32
|
try:
|
|
33
33
|
parsed = json.loads(raw)
|
|
34
34
|
except json.JSONDecodeError:
|
|
35
|
-
return
|
|
36
|
-
if
|
|
37
|
-
return "error: tool arguments must decode to a JSON object"
|
|
38
|
-
return parsed
|
|
35
|
+
return None
|
|
36
|
+
return parsed if isinstance(parsed, dict) else 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
|
|
File without changes
|
|
File without changes
|