mycode-sdk 0.9.5__tar.gz → 0.10.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.
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/PKG-INFO +1 -1
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/pyproject.toml +1 -1
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/__init__.py +2 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/agent.py +64 -14
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/compact.py +19 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/messages.py +2 -2
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/models.py +4 -4
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/providers/anthropic_like.py +3 -2
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/providers/base.py +1 -1
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/providers/gemini.py +8 -8
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/providers/openai_chat.py +2 -2
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/providers/openai_responses.py +4 -5
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/session.py +4 -7
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/tools.py +8 -15
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/.gitignore +0 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/LICENSE +0 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/README.md +0 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/attachments.py +0 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/hooks.py +0 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/models_catalog.json +0 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/py.typed +0 -0
- {mycode_sdk-0.9.5 → mycode_sdk-0.10.0}/src/mycode/utils.py +0 -0
|
@@ -4,6 +4,7 @@ from importlib import metadata
|
|
|
4
4
|
|
|
5
5
|
from mycode.agent import Agent, Event, PersistCallback, RunResult
|
|
6
6
|
from mycode.attachments import Attachment
|
|
7
|
+
from mycode.compact import NothingToCompactError
|
|
7
8
|
from mycode.hooks import AfterToolHook, BeforeToolHook, HookResult, Hooks, ToolHookContext
|
|
8
9
|
from mycode.messages import (
|
|
9
10
|
ContentBlock,
|
|
@@ -45,6 +46,7 @@ __all__ = [
|
|
|
45
46
|
"BeforeToolHook",
|
|
46
47
|
"HookResult",
|
|
47
48
|
"Hooks",
|
|
49
|
+
"NothingToCompactError",
|
|
48
50
|
"PersistCallback",
|
|
49
51
|
"RunResult",
|
|
50
52
|
"SessionStore",
|
|
@@ -23,7 +23,9 @@ from mycode.attachments import AttachmentLike, build_attachment_blocks
|
|
|
23
23
|
from mycode.compact import (
|
|
24
24
|
COMPACT_SUMMARY_PROMPT,
|
|
25
25
|
DEFAULT_COMPACT_THRESHOLD,
|
|
26
|
+
NothingToCompactError,
|
|
26
27
|
build_compact_event,
|
|
28
|
+
has_compactable_history,
|
|
27
29
|
should_compact,
|
|
28
30
|
)
|
|
29
31
|
from mycode.hooks import Hooks, ToolHookContext
|
|
@@ -455,6 +457,23 @@ class Agent:
|
|
|
455
457
|
block["meta"] = {**meta, "duration_ms": duration_ms}
|
|
456
458
|
return
|
|
457
459
|
|
|
460
|
+
async def _persist_message(
|
|
461
|
+
self,
|
|
462
|
+
message: ConversationMessage,
|
|
463
|
+
on_persist: PersistCallback | None,
|
|
464
|
+
) -> None:
|
|
465
|
+
"""Persist one message: caller callback first, then the SDK session store."""
|
|
466
|
+
|
|
467
|
+
if on_persist is not None:
|
|
468
|
+
# Callers may need to write related records before the SDK
|
|
469
|
+
# appends this message to its own session log.
|
|
470
|
+
await on_persist(message)
|
|
471
|
+
if self._store is None:
|
|
472
|
+
return
|
|
473
|
+
if not self._store.session_exists(self.session_id):
|
|
474
|
+
await self._store.create_session(self.session_id, cwd=self.cwd)
|
|
475
|
+
await self._store.append_message(self.session_id, message)
|
|
476
|
+
|
|
458
477
|
# ------------------------------------------------------------------
|
|
459
478
|
# Public entry points
|
|
460
479
|
# ------------------------------------------------------------------
|
|
@@ -469,15 +488,7 @@ class Agent:
|
|
|
469
488
|
"""Run the full agent loop for one user message."""
|
|
470
489
|
|
|
471
490
|
async def persist(message: ConversationMessage) -> None:
|
|
472
|
-
|
|
473
|
-
# Callers may need to write related records before the SDK
|
|
474
|
-
# appends this message to its own session log.
|
|
475
|
-
await on_persist(message)
|
|
476
|
-
if self._store is None:
|
|
477
|
-
return
|
|
478
|
-
if not self._store.session_exists(self.session_id):
|
|
479
|
-
await self._store.create_session(self.session_id, cwd=self.cwd)
|
|
480
|
-
await self._store.append_message(self.session_id, message)
|
|
491
|
+
await self._persist_message(message, on_persist)
|
|
481
492
|
|
|
482
493
|
self._cancel_event.clear()
|
|
483
494
|
|
|
@@ -671,7 +682,7 @@ class Agent:
|
|
|
671
682
|
return
|
|
672
683
|
if should_compact(total_tokens, self.context_window, self.compact_threshold):
|
|
673
684
|
try:
|
|
674
|
-
await self._compact(adapter,
|
|
685
|
+
await self._compact(adapter, on_persist)
|
|
675
686
|
yield Event("compact", {})
|
|
676
687
|
except asyncio.CancelledError:
|
|
677
688
|
yield Event("error", {"message": "cancelled"})
|
|
@@ -719,12 +730,47 @@ class Agent:
|
|
|
719
730
|
# Context compaction
|
|
720
731
|
# ------------------------------------------------------------------
|
|
721
732
|
|
|
733
|
+
async def acompact(
|
|
734
|
+
self,
|
|
735
|
+
*,
|
|
736
|
+
on_persist: PersistCallback | None = None,
|
|
737
|
+
) -> ConversationMessage:
|
|
738
|
+
"""Compact the conversation now and return the persisted compact marker.
|
|
739
|
+
|
|
740
|
+
Raises :class:`NothingToCompactError` when no new context follows the
|
|
741
|
+
latest compact marker, and :class:`asyncio.CancelledError` when
|
|
742
|
+
:meth:`cancel` stops the summary request.
|
|
743
|
+
"""
|
|
744
|
+
|
|
745
|
+
self._cancel_event.clear()
|
|
746
|
+
adapter = get_provider_adapter(self.provider)
|
|
747
|
+
return await self._compact(adapter, on_persist)
|
|
748
|
+
|
|
749
|
+
def compact(
|
|
750
|
+
self,
|
|
751
|
+
*,
|
|
752
|
+
on_persist: PersistCallback | None = None,
|
|
753
|
+
) -> ConversationMessage:
|
|
754
|
+
"""Compact the conversation synchronously; see :meth:`acompact`."""
|
|
755
|
+
|
|
756
|
+
try:
|
|
757
|
+
asyncio.get_running_loop()
|
|
758
|
+
except RuntimeError:
|
|
759
|
+
pass
|
|
760
|
+
else:
|
|
761
|
+
raise RuntimeError("Agent.compact() cannot run inside an active event loop; use Agent.acompact() instead")
|
|
762
|
+
|
|
763
|
+
return asyncio.run(self.acompact(on_persist=on_persist))
|
|
764
|
+
|
|
722
765
|
async def _compact(
|
|
723
766
|
self,
|
|
724
767
|
adapter: ProviderAdapter,
|
|
725
|
-
|
|
726
|
-
) ->
|
|
727
|
-
"""Ask the provider for a summary, persist the compact
|
|
768
|
+
on_persist: PersistCallback | None,
|
|
769
|
+
) -> ConversationMessage:
|
|
770
|
+
"""Ask the provider for a summary, persist and append the compact marker."""
|
|
771
|
+
|
|
772
|
+
if not has_compactable_history(self.messages):
|
|
773
|
+
raise NothingToCompactError("nothing to compact")
|
|
728
774
|
|
|
729
775
|
request = self._build_request(
|
|
730
776
|
tools=[],
|
|
@@ -746,6 +792,9 @@ class Agent:
|
|
|
746
792
|
if not summary_text:
|
|
747
793
|
raise ValueError("compaction produced empty summary")
|
|
748
794
|
|
|
795
|
+
if self._cancel_event.is_set():
|
|
796
|
+
raise asyncio.CancelledError
|
|
797
|
+
|
|
749
798
|
summary_total_tokens = (summary_message.get("meta") or {}).get("total_tokens")
|
|
750
799
|
compact_event = build_compact_event(
|
|
751
800
|
summary_text,
|
|
@@ -754,5 +803,6 @@ class Agent:
|
|
|
754
803
|
total_tokens=summary_total_tokens,
|
|
755
804
|
)
|
|
756
805
|
|
|
757
|
-
await
|
|
806
|
+
await self._persist_message(compact_event, on_persist)
|
|
758
807
|
self.messages.append(compact_event)
|
|
808
|
+
return compact_event
|
|
@@ -8,6 +8,11 @@ from mycode.messages import ConversationMessage, build_message, text_block
|
|
|
8
8
|
|
|
9
9
|
DEFAULT_COMPACT_THRESHOLD = 0.8
|
|
10
10
|
|
|
11
|
+
|
|
12
|
+
class NothingToCompactError(ValueError):
|
|
13
|
+
"""Raised when there is no new context to compact past the latest marker."""
|
|
14
|
+
|
|
15
|
+
|
|
11
16
|
COMPACT_SUMMARY_PROMPT = """\
|
|
12
17
|
Summarize this conversation to create a continuation document. \
|
|
13
18
|
This summary will replace the full conversation history, so it must \
|
|
@@ -65,6 +70,20 @@ def should_compact(
|
|
|
65
70
|
return last_total_tokens >= context_window * threshold
|
|
66
71
|
|
|
67
72
|
|
|
73
|
+
def has_compactable_history(messages: list[ConversationMessage]) -> bool:
|
|
74
|
+
"""True when at least one non-empty user/assistant message follows the latest compact marker."""
|
|
75
|
+
|
|
76
|
+
last_compact = -1
|
|
77
|
+
for i, message in enumerate(messages):
|
|
78
|
+
if message.get("role") == "compact":
|
|
79
|
+
last_compact = i
|
|
80
|
+
|
|
81
|
+
return any(
|
|
82
|
+
message.get("role") in ("user", "assistant") and message.get("content")
|
|
83
|
+
for message in messages[last_compact + 1 :]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
68
87
|
def build_compact_event(
|
|
69
88
|
summary_text: str,
|
|
70
89
|
*,
|
|
@@ -165,8 +165,8 @@ def flatten_message_text(message: ConversationMessage, *, include_thinking: bool
|
|
|
165
165
|
continue
|
|
166
166
|
raw_meta = block.get("meta")
|
|
167
167
|
meta = raw_meta if isinstance(raw_meta, dict) else {}
|
|
168
|
-
#
|
|
169
|
-
if meta.get("attachment"):
|
|
168
|
+
# Local payload blocks should not become session titles or history labels.
|
|
169
|
+
if meta.get("attachment") or meta.get("skill_snapshot"):
|
|
170
170
|
continue
|
|
171
171
|
btype = block.get("type")
|
|
172
172
|
if btype == "text" or (include_thinking and btype == "thinking"):
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import functools
|
|
6
5
|
import json
|
|
7
6
|
from dataclasses import dataclass, replace
|
|
7
|
+
from functools import cache
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
@@ -30,14 +30,14 @@ class ModelMetadata:
|
|
|
30
30
|
supports_pdf_input: bool | None = None
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
@
|
|
33
|
+
@cache
|
|
34
34
|
def load_models_catalog() -> dict[str, Any] | None:
|
|
35
35
|
"""Load the bundled model catalog from disk once per process."""
|
|
36
36
|
|
|
37
37
|
try:
|
|
38
38
|
data = json.loads(_MODELS_CATALOG_PATH.read_text(encoding="utf-8"))
|
|
39
|
-
except
|
|
40
|
-
|
|
39
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
40
|
+
return None
|
|
41
41
|
return data if isinstance(data, dict) else None
|
|
42
42
|
|
|
43
43
|
|
|
@@ -111,8 +111,9 @@ class AnthropicLikeAdapter(ProviderAdapter):
|
|
|
111
111
|
if not isinstance(content, list):
|
|
112
112
|
return
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
for block in reversed(content):
|
|
115
|
+
if not isinstance(block, dict):
|
|
116
|
+
continue
|
|
116
117
|
block_type = str(block.get("type") or "")
|
|
117
118
|
if block_type not in {"text", "image", "document", "tool_result"}:
|
|
118
119
|
continue
|
|
@@ -88,7 +88,7 @@ def parse_tool_call_input(raw_arguments: str) -> tuple[dict[str, Any], dict[str,
|
|
|
88
88
|
try:
|
|
89
89
|
parsed = json.loads(raw_arguments)
|
|
90
90
|
except json.JSONDecodeError:
|
|
91
|
-
|
|
91
|
+
return {}, {"raw_arguments": raw_arguments}
|
|
92
92
|
if isinstance(parsed, dict):
|
|
93
93
|
return parsed, {}
|
|
94
94
|
return {}, {"raw_arguments": raw_arguments}
|
|
@@ -84,7 +84,7 @@ class GoogleGeminiAdapter(ProviderAdapter):
|
|
|
84
84
|
finish_message = getattr(candidate, "finish_message", None) or finish_message
|
|
85
85
|
|
|
86
86
|
for part in getattr(getattr(candidate, "content", None), "parts", None) or []:
|
|
87
|
-
|
|
87
|
+
if event := self._consume_part(blocks, part):
|
|
88
88
|
yield event
|
|
89
89
|
except APIError as exc:
|
|
90
90
|
raise ValueError(str(exc)) from exc
|
|
@@ -223,7 +223,7 @@ class GoogleGeminiAdapter(ProviderAdapter):
|
|
|
223
223
|
return contents
|
|
224
224
|
|
|
225
225
|
def _build_config(self, request: ProviderRequest) -> types.GenerateContentConfig:
|
|
226
|
-
tools: list[types.Tool
|
|
226
|
+
tools: list[types.Tool] | None = None
|
|
227
227
|
if request.tools:
|
|
228
228
|
tools = [
|
|
229
229
|
types.Tool(
|
|
@@ -262,7 +262,7 @@ class GoogleGeminiAdapter(ProviderAdapter):
|
|
|
262
262
|
thinking_config=thinking_config,
|
|
263
263
|
)
|
|
264
264
|
|
|
265
|
-
def _consume_part(self, blocks: list[dict[str, Any]], part: Any) ->
|
|
265
|
+
def _consume_part(self, blocks: list[dict[str, Any]], part: Any) -> ProviderStreamEvent | None:
|
|
266
266
|
native_part = _to_json(part) or {}
|
|
267
267
|
if native_part.get("thought") is False:
|
|
268
268
|
native_part.pop("thought", None)
|
|
@@ -278,12 +278,12 @@ class GoogleGeminiAdapter(ProviderAdapter):
|
|
|
278
278
|
meta={"native": {"part": native_part}},
|
|
279
279
|
)
|
|
280
280
|
)
|
|
281
|
-
return
|
|
281
|
+
return None
|
|
282
282
|
|
|
283
283
|
text = getattr(part, "text", None)
|
|
284
284
|
if text is None or text == "":
|
|
285
285
|
if not native_part.get("thought_signature"):
|
|
286
|
-
return
|
|
286
|
+
return None
|
|
287
287
|
|
|
288
288
|
# Gemini may put the final thought signature into an empty-text part.
|
|
289
289
|
# Keep it as a separate empty block so replay preserves the original
|
|
@@ -291,7 +291,7 @@ class GoogleGeminiAdapter(ProviderAdapter):
|
|
|
291
291
|
part_meta = {"native": {"part": native_part}}
|
|
292
292
|
is_thought = getattr(part, "thought", False)
|
|
293
293
|
blocks.append(thinking_block("", meta=part_meta) if is_thought else text_block("", meta=part_meta))
|
|
294
|
-
return
|
|
294
|
+
return None
|
|
295
295
|
|
|
296
296
|
is_thought = bool(getattr(part, "thought", False))
|
|
297
297
|
event = ProviderStreamEvent("thinking_delta" if is_thought else "text_delta", {"text": str(text)})
|
|
@@ -310,10 +310,10 @@ class GoogleGeminiAdapter(ProviderAdapter):
|
|
|
310
310
|
last_part["text"] = f"{last_part.get('text') or ''}{text}"
|
|
311
311
|
if current_signature and not last_signature:
|
|
312
312
|
last_part["thought_signature"] = current_signature
|
|
313
|
-
return
|
|
313
|
+
return event
|
|
314
314
|
|
|
315
315
|
part_meta = {"native": {"part": native_part}}
|
|
316
316
|
blocks.append(
|
|
317
317
|
thinking_block(str(text), meta=part_meta) if is_thought else text_block(str(text), meta=part_meta)
|
|
318
318
|
)
|
|
319
|
-
return
|
|
319
|
+
return event
|
|
@@ -9,7 +9,7 @@ from typing import Any, override
|
|
|
9
9
|
|
|
10
10
|
from openai import APIError, AsyncOpenAI
|
|
11
11
|
|
|
12
|
-
from mycode.messages import assistant_message, text_block, thinking_block, tool_use_block
|
|
12
|
+
from mycode.messages import ConversationMessage, assistant_message, text_block, thinking_block, tool_use_block
|
|
13
13
|
from mycode.providers.base import (
|
|
14
14
|
DEFAULT_REQUEST_TIMEOUT,
|
|
15
15
|
ProviderAdapter,
|
|
@@ -174,7 +174,7 @@ class OpenAIChatAdapter(ProviderAdapter):
|
|
|
174
174
|
},
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
def _serialize_message(self, message:
|
|
177
|
+
def _serialize_message(self, message: ConversationMessage) -> list[dict[str, Any]]:
|
|
178
178
|
"""Convert one canonical message into Chat Completions wire messages."""
|
|
179
179
|
|
|
180
180
|
role = str(message.get("role") or "user")
|
|
@@ -102,9 +102,8 @@ class OpenAIResponsesAdapter(ProviderAdapter):
|
|
|
102
102
|
raise ValueError(str(exc)) from exc
|
|
103
103
|
|
|
104
104
|
def _build_request_payload(self, request: ProviderRequest) -> dict[str, Any]:
|
|
105
|
-
prepared_messages = self.prepare_messages(request)
|
|
106
105
|
input_items: list[dict[str, Any]] = []
|
|
107
|
-
for message in
|
|
106
|
+
for message in self.prepare_messages(request):
|
|
108
107
|
role = message.get("role")
|
|
109
108
|
if role == "user":
|
|
110
109
|
input_items.extend(self._serialize_user_message(message))
|
|
@@ -262,7 +261,7 @@ class OpenAIResponsesAdapter(ProviderAdapter):
|
|
|
262
261
|
response: Any,
|
|
263
262
|
*,
|
|
264
263
|
output_items: list[Any] | None = None,
|
|
265
|
-
) ->
|
|
264
|
+
) -> ConversationMessage:
|
|
266
265
|
raw_output = output_items if output_items is not None else (getattr(response, "output", None) or [])
|
|
267
266
|
dumped_output_items = dump_model(raw_output)
|
|
268
267
|
blocks: list[dict[str, Any]] = []
|
|
@@ -357,8 +356,8 @@ def _normalize_strict_schema(schema: Any) -> None:
|
|
|
357
356
|
|
|
358
357
|
properties = schema.get("properties")
|
|
359
358
|
if isinstance(properties, dict):
|
|
360
|
-
required_names = {
|
|
361
|
-
for name, property_schema in
|
|
359
|
+
required_names = {name for name in schema.get("required", []) if isinstance(name, str)}
|
|
360
|
+
for name, property_schema in properties.items():
|
|
362
361
|
_normalize_strict_schema(property_schema)
|
|
363
362
|
if name not in required_names:
|
|
364
363
|
properties[name] = _nullable_schema(property_schema)
|
|
@@ -139,7 +139,7 @@ class SessionStore:
|
|
|
139
139
|
meta = _project_meta(meta)
|
|
140
140
|
self.meta_path(session_id).write_text(json.dumps(meta, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
141
141
|
index = self._read_index()
|
|
142
|
-
index[session_id] =
|
|
142
|
+
index[session_id] = meta
|
|
143
143
|
self._write_index(index)
|
|
144
144
|
|
|
145
145
|
def _read_index(self) -> SessionIndex:
|
|
@@ -161,7 +161,7 @@ class SessionStore:
|
|
|
161
161
|
continue
|
|
162
162
|
meta = self._read_meta(entry.name)
|
|
163
163
|
if meta is not None:
|
|
164
|
-
index[entry.name] =
|
|
164
|
+
index[entry.name] = meta
|
|
165
165
|
self._write_index(index)
|
|
166
166
|
return index
|
|
167
167
|
|
|
@@ -246,9 +246,7 @@ class SessionStore:
|
|
|
246
246
|
|
|
247
247
|
async def delete_session(self, session_id: str) -> None:
|
|
248
248
|
def delete() -> None:
|
|
249
|
-
|
|
250
|
-
if sdir.exists():
|
|
251
|
-
shutil.rmtree(sdir, ignore_errors=True)
|
|
249
|
+
shutil.rmtree(self.session_dir(session_id), ignore_errors=True)
|
|
252
250
|
index = self._read_index()
|
|
253
251
|
index.pop(session_id, None)
|
|
254
252
|
self._write_index(index)
|
|
@@ -285,8 +283,7 @@ class SessionStore:
|
|
|
285
283
|
|
|
286
284
|
def append() -> None:
|
|
287
285
|
with self.messages_path(session_id).open("a", encoding="utf-8") as handle:
|
|
288
|
-
handle.write(json.dumps(message, ensure_ascii=False))
|
|
289
|
-
handle.write("\n")
|
|
286
|
+
handle.write(json.dumps(message, ensure_ascii=False) + "\n")
|
|
290
287
|
|
|
291
288
|
meta = self._read_meta(session_id)
|
|
292
289
|
if meta is None:
|
|
@@ -129,12 +129,6 @@ class ToolContext:
|
|
|
129
129
|
|
|
130
130
|
return self.executor.execute(name, args, self)
|
|
131
131
|
|
|
132
|
-
def track_proc(self, proc: subprocess.Popen[str]) -> None:
|
|
133
|
-
self.executor.track_proc(proc)
|
|
134
|
-
|
|
135
|
-
def untrack_proc(self, proc: subprocess.Popen[str]) -> None:
|
|
136
|
-
self.executor.untrack_proc(proc)
|
|
137
|
-
|
|
138
132
|
|
|
139
133
|
# ---------------------------------------------------------------------------
|
|
140
134
|
# ToolExecutor
|
|
@@ -202,6 +196,9 @@ class ToolExecutor:
|
|
|
202
196
|
# agents may run concurrently in the same process); this module-level set is
|
|
203
197
|
# a shutdown-time safety net exposed as ``cancel_all_tools``.
|
|
204
198
|
|
|
199
|
+
_ACTIVE_PROCS: set[subprocess.Popen[str]] = set()
|
|
200
|
+
_ACTIVE_PROCS_LOCK = threading.Lock()
|
|
201
|
+
|
|
205
202
|
|
|
206
203
|
def cancel_all_tools() -> None:
|
|
207
204
|
"""Terminate every running bash subprocess in the current process."""
|
|
@@ -213,10 +210,6 @@ def cancel_all_tools() -> None:
|
|
|
213
210
|
_kill_proc_tree(proc)
|
|
214
211
|
|
|
215
212
|
|
|
216
|
-
_ACTIVE_PROCS: set[subprocess.Popen[str]] = set()
|
|
217
|
-
_ACTIVE_PROCS_LOCK = threading.Lock()
|
|
218
|
-
|
|
219
|
-
|
|
220
213
|
def _kill_proc_tree(proc: subprocess.Popen[str]) -> None:
|
|
221
214
|
try:
|
|
222
215
|
if os.name == "posix":
|
|
@@ -479,8 +472,8 @@ def read_tool(
|
|
|
479
472
|
],
|
|
480
473
|
)
|
|
481
474
|
|
|
482
|
-
start_line = offset if
|
|
483
|
-
line_limit = limit if
|
|
475
|
+
start_line = offset if offset is not None and offset > 0 else 1
|
|
476
|
+
line_limit = limit if limit is not None and limit > 0 else DEFAULT_MAX_LINES
|
|
484
477
|
lines: list[str] = []
|
|
485
478
|
total_lines = 0
|
|
486
479
|
next_offset: int | None = None
|
|
@@ -764,7 +757,7 @@ def bash_tool(ctx: ToolContext, command: str, timeout: int | None = None) -> Too
|
|
|
764
757
|
again by :func:`truncate_text` to the display limits.
|
|
765
758
|
"""
|
|
766
759
|
|
|
767
|
-
timeout_seconds =
|
|
760
|
+
timeout_seconds = timeout if timeout is not None and timeout > 0 else BASH_TIMEOUT_SECONDS
|
|
768
761
|
|
|
769
762
|
proc: subprocess.Popen[str] | None = None
|
|
770
763
|
log_path = ctx.tool_output_dir / f"bash-{ctx.tool_call_id or 'call'}.log"
|
|
@@ -787,7 +780,7 @@ def bash_tool(ctx: ToolContext, command: str, timeout: int | None = None) -> Too
|
|
|
787
780
|
bufsize=1,
|
|
788
781
|
start_new_session=os.name == "posix",
|
|
789
782
|
)
|
|
790
|
-
ctx.track_proc(proc)
|
|
783
|
+
ctx.executor.track_proc(proc)
|
|
791
784
|
|
|
792
785
|
stdout = cast(TextIO, proc.stdout)
|
|
793
786
|
output_queue: queue.Queue[str | None] = queue.Queue()
|
|
@@ -903,7 +896,7 @@ def bash_tool(ctx: ToolContext, command: str, timeout: int | None = None) -> Too
|
|
|
903
896
|
with suppress(Exception):
|
|
904
897
|
log_file.close()
|
|
905
898
|
if proc is not None:
|
|
906
|
-
ctx.untrack_proc(proc)
|
|
899
|
+
ctx.executor.untrack_proc(proc)
|
|
907
900
|
if proc.poll() is None:
|
|
908
901
|
_kill_proc_tree(proc)
|
|
909
902
|
|
|
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
|