mycode-sdk 0.5.8__tar.gz → 0.7.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.5.8 → mycode_sdk-0.7.0}/PKG-INFO +29 -1
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/README.md +28 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/pyproject.toml +1 -1
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/__init__.py +6 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/agent.py +58 -4
- mycode_sdk-0.7.0/src/mycode/hooks.py +97 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/models_catalog.json +7 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/session.py +3 -94
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/.gitignore +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/LICENSE +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/messages.py +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/models.py +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/providers/__init__.py +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/providers/anthropic_like.py +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/providers/base.py +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/providers/gemini.py +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/providers/openai_chat.py +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/providers/openai_responses.py +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/py.typed +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/tools.py +0 -0
- {mycode_sdk-0.5.8 → mycode_sdk-0.7.0}/src/mycode/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycode-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -139,4 +139,32 @@ def summarize_file(ctx: ToolContext, path: str) -> str:
|
|
|
139
139
|
return result.output.splitlines()[0] if result.output else ""
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
## Tool hooks
|
|
143
|
+
|
|
144
|
+
Inspect or replace tool calls before they run. Return `None` from `before_tool` to let the tool execute, or a `ToolExecutionResult` to skip it:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from mycode import Agent, Hooks, ToolExecutionResult, bash_tool
|
|
148
|
+
|
|
149
|
+
hooks = Hooks()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@hooks.before_tool
|
|
153
|
+
async def block_rm(ctx):
|
|
154
|
+
cmd = str(ctx.tool_input.get("command") or "")
|
|
155
|
+
if ctx.tool_name == "bash" and "rm -rf" in cmd:
|
|
156
|
+
return ToolExecutionResult(output="error: blocked", is_error=True)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
agent = Agent(
|
|
161
|
+
model="claude-sonnet-4-6",
|
|
162
|
+
api_key="...",
|
|
163
|
+
tools=[bash_tool],
|
|
164
|
+
hooks=hooks,
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`@hooks.after_tool` runs after the tool and can replace the result (audit, redact, etc.).
|
|
169
|
+
|
|
142
170
|
See [docs/sdk.md](../docs/sdk.md) for the event stream, cancellation, sessions, and the full `Agent` / `@tool` reference.
|
|
@@ -114,4 +114,32 @@ def summarize_file(ctx: ToolContext, path: str) -> str:
|
|
|
114
114
|
return result.output.splitlines()[0] if result.output else ""
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
## Tool hooks
|
|
118
|
+
|
|
119
|
+
Inspect or replace tool calls before they run. Return `None` from `before_tool` to let the tool execute, or a `ToolExecutionResult` to skip it:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from mycode import Agent, Hooks, ToolExecutionResult, bash_tool
|
|
123
|
+
|
|
124
|
+
hooks = Hooks()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@hooks.before_tool
|
|
128
|
+
async def block_rm(ctx):
|
|
129
|
+
cmd = str(ctx.tool_input.get("command") or "")
|
|
130
|
+
if ctx.tool_name == "bash" and "rm -rf" in cmd:
|
|
131
|
+
return ToolExecutionResult(output="error: blocked", is_error=True)
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
agent = Agent(
|
|
136
|
+
model="claude-sonnet-4-6",
|
|
137
|
+
api_key="...",
|
|
138
|
+
tools=[bash_tool],
|
|
139
|
+
hooks=hooks,
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`@hooks.after_tool` runs after the tool and can replace the result (audit, redact, etc.).
|
|
144
|
+
|
|
117
145
|
See [docs/sdk.md](../docs/sdk.md) for the event stream, cancellation, sessions, and the full `Agent` / `@tool` reference.
|
|
@@ -10,6 +10,7 @@ silently exposing file system and shell access.
|
|
|
10
10
|
from importlib import metadata
|
|
11
11
|
|
|
12
12
|
from mycode.agent import Agent, Event, PersistCallback, RunResult
|
|
13
|
+
from mycode.hooks import AfterToolHook, BeforeToolHook, HookResult, Hooks, ToolHookContext
|
|
13
14
|
from mycode.messages import (
|
|
14
15
|
ContentBlock,
|
|
15
16
|
ConversationMessage,
|
|
@@ -49,12 +50,17 @@ __all__ = [
|
|
|
49
50
|
"ConversationMessage",
|
|
50
51
|
"DEFAULT_TOOL_SPECS",
|
|
51
52
|
"Event",
|
|
53
|
+
"AfterToolHook",
|
|
54
|
+
"BeforeToolHook",
|
|
55
|
+
"HookResult",
|
|
56
|
+
"Hooks",
|
|
52
57
|
"PersistCallback",
|
|
53
58
|
"RunResult",
|
|
54
59
|
"SessionStore",
|
|
55
60
|
"ToolContext",
|
|
56
61
|
"ToolExecutionResult",
|
|
57
62
|
"ToolExecutor",
|
|
63
|
+
"ToolHookContext",
|
|
58
64
|
"ToolSpec",
|
|
59
65
|
"__version__",
|
|
60
66
|
"assistant_message",
|
|
@@ -17,6 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
from typing import Any, cast
|
|
18
18
|
from uuid import uuid4
|
|
19
19
|
|
|
20
|
+
from mycode.hooks import Hooks, ToolHookContext
|
|
20
21
|
from mycode.messages import (
|
|
21
22
|
ConversationMessage,
|
|
22
23
|
build_message,
|
|
@@ -83,6 +84,7 @@ class Agent:
|
|
|
83
84
|
supports_pdf_input: bool | None = None,
|
|
84
85
|
system: str = "",
|
|
85
86
|
tools: Sequence[ToolSpec] = (),
|
|
87
|
+
hooks: Hooks | None = None,
|
|
86
88
|
):
|
|
87
89
|
self.model = model
|
|
88
90
|
if provider is None:
|
|
@@ -109,6 +111,7 @@ class Agent:
|
|
|
109
111
|
self.reasoning_effort = reasoning_effort
|
|
110
112
|
|
|
111
113
|
self.system = system
|
|
114
|
+
self.hooks = hooks or Hooks()
|
|
112
115
|
self._cancel_event = asyncio.Event()
|
|
113
116
|
self._provider_event_task: asyncio.Future[ProviderStreamEvent] | None = None
|
|
114
117
|
|
|
@@ -210,6 +213,8 @@ class Agent:
|
|
|
210
213
|
raw_args = tool_use.get("input")
|
|
211
214
|
args = raw_args if isinstance(raw_args, dict) else {}
|
|
212
215
|
|
|
216
|
+
# Surface the tool to the UI before running hooks so the call is
|
|
217
|
+
# always visible — even when a hook is awaiting a permission decision.
|
|
213
218
|
yield Event("tool_start", {"tool_call": {"id": tool_id, "name": name, "input": args}})
|
|
214
219
|
|
|
215
220
|
if self._cancel_event.is_set():
|
|
@@ -227,8 +232,38 @@ class Agent:
|
|
|
227
232
|
)
|
|
228
233
|
return
|
|
229
234
|
|
|
235
|
+
hook_ctx = ToolHookContext(
|
|
236
|
+
session_id=self.session_id,
|
|
237
|
+
cwd=self.cwd,
|
|
238
|
+
provider=self.provider,
|
|
239
|
+
model=self.model,
|
|
240
|
+
tool_call_id=tool_id,
|
|
241
|
+
tool_name=name,
|
|
242
|
+
tool_input=args,
|
|
243
|
+
tool=spec,
|
|
244
|
+
)
|
|
245
|
+
try:
|
|
246
|
+
result = await self.hooks.run_before_tool(hook_ctx)
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
yield self._tool_done_event(
|
|
249
|
+
tool_id,
|
|
250
|
+
ToolExecutionResult(output=f"error: tool hook failed: {exc}", is_error=True),
|
|
251
|
+
)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
if result is not None:
|
|
255
|
+
yield await self._finish_tool_call(tool_id, hook_ctx, result)
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
if self._cancel_event.is_set():
|
|
259
|
+
yield self._tool_done_event(
|
|
260
|
+
tool_id,
|
|
261
|
+
ToolExecutionResult(output="error: cancelled", is_error=True),
|
|
262
|
+
)
|
|
263
|
+
return
|
|
264
|
+
|
|
230
265
|
if spec.streams_output:
|
|
231
|
-
async for event in self._run_streaming_tool(tool_id=tool_id, name=name, args=args):
|
|
266
|
+
async for event in self._run_streaming_tool(tool_id=tool_id, name=name, args=args, hook_ctx=hook_ctx):
|
|
232
267
|
yield event
|
|
233
268
|
return
|
|
234
269
|
|
|
@@ -238,7 +273,7 @@ class Agent:
|
|
|
238
273
|
except Exception as exc: # pragma: no cover - defensive
|
|
239
274
|
result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
|
|
240
275
|
|
|
241
|
-
yield self.
|
|
276
|
+
yield await self._finish_tool_call(tool_id, hook_ctx, result)
|
|
242
277
|
|
|
243
278
|
async def _run_streaming_tool(
|
|
244
279
|
self,
|
|
@@ -246,6 +281,7 @@ class Agent:
|
|
|
246
281
|
tool_id: str,
|
|
247
282
|
name: str,
|
|
248
283
|
args: dict[str, Any],
|
|
284
|
+
hook_ctx: ToolHookContext,
|
|
249
285
|
) -> AsyncIterator[Event]:
|
|
250
286
|
"""Run one streaming tool, forwarding ``tool_output`` events live."""
|
|
251
287
|
|
|
@@ -289,13 +325,31 @@ class Agent:
|
|
|
289
325
|
except Exception:
|
|
290
326
|
pass
|
|
291
327
|
result = ToolExecutionResult(output="error: cancelled", is_error=True)
|
|
328
|
+
yield self._tool_done_event(tool_id, result)
|
|
329
|
+
return
|
|
292
330
|
else:
|
|
293
331
|
try:
|
|
294
332
|
result = await task
|
|
295
333
|
except Exception as exc: # pragma: no cover - defensive
|
|
296
334
|
result = ToolExecutionResult(output=f"error: {exc}", is_error=True)
|
|
297
335
|
|
|
298
|
-
yield self.
|
|
336
|
+
yield await self._finish_tool_call(tool_id, hook_ctx, result)
|
|
337
|
+
|
|
338
|
+
async def _finish_tool_call(
|
|
339
|
+
self,
|
|
340
|
+
tool_id: str,
|
|
341
|
+
hook_ctx: ToolHookContext,
|
|
342
|
+
result: ToolExecutionResult,
|
|
343
|
+
) -> Event:
|
|
344
|
+
try:
|
|
345
|
+
result = await self.hooks.run_after_tool(hook_ctx, result)
|
|
346
|
+
except Exception:
|
|
347
|
+
logger.exception(
|
|
348
|
+
"after_tool hook failed for %s (call %s)",
|
|
349
|
+
hook_ctx.tool_name,
|
|
350
|
+
hook_ctx.tool_call_id,
|
|
351
|
+
)
|
|
352
|
+
return self._tool_done_event(tool_id, result)
|
|
299
353
|
|
|
300
354
|
def _ctx_for_call(
|
|
301
355
|
self,
|
|
@@ -530,7 +584,7 @@ class Agent:
|
|
|
530
584
|
)
|
|
531
585
|
)
|
|
532
586
|
|
|
533
|
-
if
|
|
587
|
+
if self._cancel_event.is_set():
|
|
534
588
|
tool_result_message = build_message("user", tool_results)
|
|
535
589
|
self.messages.append(tool_result_message)
|
|
536
590
|
await persist(tool_result_message)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Tool execution hooks for the agent runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from mycode.tools import ToolExecutionResult, ToolSpec
|
|
12
|
+
|
|
13
|
+
type HookResult = ToolExecutionResult | None
|
|
14
|
+
type BeforeToolHook = Callable[["ToolHookContext"], HookResult | Awaitable[HookResult]]
|
|
15
|
+
type AfterToolHook = Callable[["ToolHookContext", ToolExecutionResult], HookResult | Awaitable[HookResult]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ToolHookContext:
|
|
20
|
+
"""Read-only context passed to tool execution hooks."""
|
|
21
|
+
|
|
22
|
+
session_id: str
|
|
23
|
+
cwd: str
|
|
24
|
+
provider: str
|
|
25
|
+
model: str
|
|
26
|
+
tool_call_id: str
|
|
27
|
+
tool_name: str
|
|
28
|
+
tool_input: Mapping[str, Any]
|
|
29
|
+
tool: ToolSpec
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
object.__setattr__(self, "tool_input", _freeze(self.tool_input))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Hooks:
|
|
36
|
+
"""Ordered callbacks around model-requested tool execution."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
before_tool: Sequence[BeforeToolHook] = (),
|
|
42
|
+
after_tool: Sequence[AfterToolHook] = (),
|
|
43
|
+
) -> None:
|
|
44
|
+
self._before_tool = list(before_tool)
|
|
45
|
+
self._after_tool = list(after_tool)
|
|
46
|
+
|
|
47
|
+
def before_tool(self, hook: BeforeToolHook) -> BeforeToolHook:
|
|
48
|
+
"""Register a hook that can return a tool result before execution."""
|
|
49
|
+
|
|
50
|
+
self._before_tool.append(hook)
|
|
51
|
+
return hook
|
|
52
|
+
|
|
53
|
+
def after_tool(self, hook: AfterToolHook) -> AfterToolHook:
|
|
54
|
+
"""Register a hook that can observe or replace a tool result."""
|
|
55
|
+
|
|
56
|
+
self._after_tool.append(hook)
|
|
57
|
+
return hook
|
|
58
|
+
|
|
59
|
+
async def run_before_tool(self, ctx: ToolHookContext) -> ToolExecutionResult | None:
|
|
60
|
+
for hook in self._before_tool:
|
|
61
|
+
result = await _resolve(hook(ctx))
|
|
62
|
+
if result is not None:
|
|
63
|
+
return result
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
async def run_after_tool(self, ctx: ToolHookContext, result: ToolExecutionResult) -> ToolExecutionResult:
|
|
67
|
+
for hook in self._after_tool:
|
|
68
|
+
replacement = await _resolve(hook(ctx, result))
|
|
69
|
+
if replacement is not None:
|
|
70
|
+
result = replacement
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _resolve(value: object) -> HookResult:
|
|
75
|
+
if inspect.isawaitable(value):
|
|
76
|
+
value = await value
|
|
77
|
+
if value is None or isinstance(value, ToolExecutionResult):
|
|
78
|
+
return value
|
|
79
|
+
raise TypeError(f"tool hook returned unsupported value: {type(value).__name__}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _freeze(value: Any) -> Any:
|
|
83
|
+
"""Recursively wrap mappings in MappingProxyType and lists/tuples in tuples."""
|
|
84
|
+
if isinstance(value, Mapping):
|
|
85
|
+
return MappingProxyType({str(k): _freeze(v) for k, v in value.items()})
|
|
86
|
+
if isinstance(value, (list, tuple)):
|
|
87
|
+
return tuple(_freeze(item) for item in value)
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"AfterToolHook",
|
|
93
|
+
"BeforeToolHook",
|
|
94
|
+
"HookResult",
|
|
95
|
+
"Hooks",
|
|
96
|
+
"ToolHookContext",
|
|
97
|
+
]
|
|
@@ -177,6 +177,13 @@
|
|
|
177
177
|
"supports_pdf_input": false,
|
|
178
178
|
"supports_reasoning": true
|
|
179
179
|
},
|
|
180
|
+
"deepseek-v4-flash": {
|
|
181
|
+
"context_window": 1000000,
|
|
182
|
+
"max_output_tokens": 384000,
|
|
183
|
+
"supports_image_input": false,
|
|
184
|
+
"supports_pdf_input": false,
|
|
185
|
+
"supports_reasoning": true
|
|
186
|
+
},
|
|
180
187
|
"deepseek-v4-pro": {
|
|
181
188
|
"context_window": 1000000,
|
|
182
189
|
"max_output_tokens": 384000,
|
|
@@ -18,7 +18,7 @@ from datetime import UTC, datetime
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Any, TypedDict, cast
|
|
20
20
|
|
|
21
|
-
from mycode.messages import ConversationMessage, build_message, flatten_message_text, text_block
|
|
21
|
+
from mycode.messages import ConversationMessage, build_message, flatten_message_text, text_block
|
|
22
22
|
|
|
23
23
|
# ---------------------------------------------------------------------
|
|
24
24
|
# Session format and compacting defaults
|
|
@@ -315,10 +315,10 @@ class SessionStore:
|
|
|
315
315
|
# Replay order defines the visible conversation state.
|
|
316
316
|
# 1) compact rewrites older history into one summary view
|
|
317
317
|
# 2) rewind truncates that visible list by message index
|
|
318
|
-
#
|
|
318
|
+
# Orphan tool_use blocks (e.g. left open by a server crash) are
|
|
319
|
+
# closed by the provider adapter at replay time, not here.
|
|
319
320
|
visible_messages = apply_compact(raw_messages)
|
|
320
321
|
visible_messages = apply_rewind(visible_messages)
|
|
321
|
-
self._repair_interrupted_tool_loop(session_id, meta, visible_messages)
|
|
322
322
|
|
|
323
323
|
return {"session": self._summary(session_id, meta), "messages": visible_messages}
|
|
324
324
|
|
|
@@ -387,94 +387,3 @@ class SessionStore:
|
|
|
387
387
|
"""Return meta augmented with the session id for API responses."""
|
|
388
388
|
|
|
389
389
|
return {"id": session_id, **meta}
|
|
390
|
-
|
|
391
|
-
# ---------------------------------------------------------------------
|
|
392
|
-
# Interrupted tool repair
|
|
393
|
-
# ---------------------------------------------------------------------
|
|
394
|
-
|
|
395
|
-
def _repair_interrupted_tool_loop(
|
|
396
|
-
self,
|
|
397
|
-
session_id: str,
|
|
398
|
-
meta: SessionMetaDict,
|
|
399
|
-
messages: list[ConversationMessage],
|
|
400
|
-
) -> None:
|
|
401
|
-
"""Append a synthetic tool result when the latest tool loop was interrupted.
|
|
402
|
-
|
|
403
|
-
The runtime persists sessions as append-only JSONL. If a previous run was
|
|
404
|
-
interrupted after an assistant emitted `tool_use` blocks but before a
|
|
405
|
-
matching `tool_result` user message was written, repair the session by
|
|
406
|
-
appending one synthetic error result message. ``meta`` is mutated in
|
|
407
|
-
place so the caller's view stays consistent with the updated file.
|
|
408
|
-
"""
|
|
409
|
-
|
|
410
|
-
pending_tool_use_ids: list[str] = []
|
|
411
|
-
pending_tool_call_index: int | None = None
|
|
412
|
-
|
|
413
|
-
# Find the latest assistant message that started a tool loop.
|
|
414
|
-
for index in range(len(messages) - 1, -1, -1):
|
|
415
|
-
message = messages[index]
|
|
416
|
-
if message.get("role") != "assistant":
|
|
417
|
-
continue
|
|
418
|
-
|
|
419
|
-
blocks = message.get("content")
|
|
420
|
-
if not isinstance(blocks, list):
|
|
421
|
-
continue
|
|
422
|
-
|
|
423
|
-
tool_use_ids = [
|
|
424
|
-
str(block.get("id") or "")
|
|
425
|
-
for block in blocks
|
|
426
|
-
if isinstance(block, dict) and block.get("type") == "tool_use" and block.get("id")
|
|
427
|
-
]
|
|
428
|
-
if not tool_use_ids:
|
|
429
|
-
continue
|
|
430
|
-
|
|
431
|
-
pending_tool_use_ids = tool_use_ids
|
|
432
|
-
pending_tool_call_index = index
|
|
433
|
-
break
|
|
434
|
-
|
|
435
|
-
if pending_tool_call_index is None:
|
|
436
|
-
return
|
|
437
|
-
|
|
438
|
-
# Collect tool results that were recorded after the assistant message.
|
|
439
|
-
completed_tool_use_ids: set[str] = set()
|
|
440
|
-
for message in messages[pending_tool_call_index + 1 :]:
|
|
441
|
-
if message.get("role") != "user":
|
|
442
|
-
continue
|
|
443
|
-
|
|
444
|
-
blocks = message.get("content")
|
|
445
|
-
if not isinstance(blocks, list):
|
|
446
|
-
continue
|
|
447
|
-
|
|
448
|
-
for block in blocks:
|
|
449
|
-
if not isinstance(block, dict) or block.get("type") != "tool_result":
|
|
450
|
-
continue
|
|
451
|
-
tool_use_id = str(block.get("tool_use_id") or "")
|
|
452
|
-
if tool_use_id:
|
|
453
|
-
completed_tool_use_ids.add(tool_use_id)
|
|
454
|
-
|
|
455
|
-
missing_tool_use_ids = [
|
|
456
|
-
tool_use_id for tool_use_id in pending_tool_use_ids if tool_use_id not in completed_tool_use_ids
|
|
457
|
-
]
|
|
458
|
-
if not missing_tool_use_ids:
|
|
459
|
-
return
|
|
460
|
-
|
|
461
|
-
repair_message = build_message(
|
|
462
|
-
"user",
|
|
463
|
-
[
|
|
464
|
-
tool_result_block(
|
|
465
|
-
tool_use_id=tool_use_id,
|
|
466
|
-
output="error: tool call was interrupted",
|
|
467
|
-
is_error=True,
|
|
468
|
-
)
|
|
469
|
-
for tool_use_id in missing_tool_use_ids
|
|
470
|
-
],
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
with self.messages_path(session_id).open("a", encoding="utf-8") as handle:
|
|
474
|
-
handle.write(json.dumps(repair_message, ensure_ascii=False))
|
|
475
|
-
handle.write("\n")
|
|
476
|
-
|
|
477
|
-
meta["updated_at"] = _now()
|
|
478
|
-
self._write_meta(session_id, meta)
|
|
479
|
-
|
|
480
|
-
messages.append(repair_message)
|
|
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
|