morphsdk 0.2.5__py3-none-any.whl

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.
Files changed (61) hide show
  1. morphsdk/__init__.py +54 -0
  2. morphsdk/_agent/__init__.py +64 -0
  3. morphsdk/_agent/config.py +52 -0
  4. morphsdk/_agent/explore.py +276 -0
  5. morphsdk/_agent/github.py +57 -0
  6. morphsdk/_agent/helpers.py +133 -0
  7. morphsdk/_agent/parser.py +163 -0
  8. morphsdk/_agent/runner.py +524 -0
  9. morphsdk/_agent/tools.py +171 -0
  10. morphsdk/_agent/types.py +126 -0
  11. morphsdk/_base.py +309 -0
  12. morphsdk/_client.py +245 -0
  13. morphsdk/_config.py +37 -0
  14. morphsdk/_constants.py +53 -0
  15. morphsdk/_errors.py +111 -0
  16. morphsdk/_providers/__init__.py +36 -0
  17. morphsdk/_providers/_filter.py +92 -0
  18. morphsdk/_providers/base.py +94 -0
  19. morphsdk/_providers/code_storage_http.py +104 -0
  20. morphsdk/_providers/local.py +270 -0
  21. morphsdk/_providers/remote.py +161 -0
  22. morphsdk/_version.py +1 -0
  23. morphsdk/adapters/__init__.py +1 -0
  24. morphsdk/adapters/anthropic.py +360 -0
  25. morphsdk/adapters/langchain.py +120 -0
  26. morphsdk/adapters/openai.py +500 -0
  27. morphsdk/py.typed +0 -0
  28. morphsdk/resources/__init__.py +0 -0
  29. morphsdk/resources/browser.py +919 -0
  30. morphsdk/resources/compact.py +133 -0
  31. morphsdk/resources/edit.py +506 -0
  32. morphsdk/resources/explore.py +333 -0
  33. morphsdk/resources/git.py +861 -0
  34. morphsdk/resources/github.py +1214 -0
  35. morphsdk/resources/grep.py +583 -0
  36. morphsdk/resources/mobile.py +134 -0
  37. morphsdk/resources/reflex.py +414 -0
  38. morphsdk/resources/router.py +124 -0
  39. morphsdk/resources/search.py +110 -0
  40. morphsdk/tracing/__init__.py +70 -0
  41. morphsdk/tracing/_otel.py +101 -0
  42. morphsdk/tracing/core.py +249 -0
  43. morphsdk/tracing/interaction.py +284 -0
  44. morphsdk/tracing/otel.py +75 -0
  45. morphsdk/tracing/reflex.py +58 -0
  46. morphsdk/tracing/types.py +163 -0
  47. morphsdk/types/__init__.py +140 -0
  48. morphsdk/types/browser.py +118 -0
  49. morphsdk/types/compact.py +41 -0
  50. morphsdk/types/edit.py +31 -0
  51. morphsdk/types/explore.py +42 -0
  52. morphsdk/types/git.py +25 -0
  53. morphsdk/types/github.py +111 -0
  54. morphsdk/types/grep.py +41 -0
  55. morphsdk/types/mobile.py +25 -0
  56. morphsdk/types/reflex.py +137 -0
  57. morphsdk/types/router.py +21 -0
  58. morphsdk/types/search.py +33 -0
  59. morphsdk-0.2.5.dist-info/METADATA +226 -0
  60. morphsdk-0.2.5.dist-info/RECORD +61 -0
  61. morphsdk-0.2.5.dist-info/WHEEL +4 -0
@@ -0,0 +1,133 @@
1
+ """Compact resource -- context compression with line-range awareness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from morphsdk._constants import API_BASE_URL, COMPACT_MODEL, COMPACT_TIMEOUT
8
+ from morphsdk.types.compact import CompactResult
9
+
10
+ if TYPE_CHECKING:
11
+ from morphsdk._base import AsyncBaseClient, BaseClient
12
+
13
+
14
+ def _build_compact_body(
15
+ *,
16
+ input: str | list[dict[str, Any]] | None,
17
+ messages: list[dict[str, Any]] | None,
18
+ query: str | None,
19
+ compression_ratio: float,
20
+ preserve_recent: int,
21
+ include_line_ranges: bool,
22
+ include_markers: bool,
23
+ model: str | None,
24
+ ) -> dict[str, Any]:
25
+ """Build the ``/v1/compact`` request body.
26
+
27
+ Normalizes the *messages*/*input* variants: *messages* takes priority,
28
+ then a string *input*, then a list *input*. Raises
29
+ :class:`~morphsdk._errors.ValidationError` if neither is provided.
30
+ """
31
+ body: dict[str, Any] = {
32
+ "compression_ratio": compression_ratio,
33
+ "preserve_recent": preserve_recent,
34
+ "model": model or COMPACT_MODEL,
35
+ "include_line_ranges": include_line_ranges,
36
+ "include_markers": include_markers,
37
+ }
38
+
39
+ if query is not None:
40
+ body["query"] = query
41
+
42
+ if messages is not None:
43
+ body["messages"] = messages
44
+ elif isinstance(input, str):
45
+ body["input"] = input
46
+ elif isinstance(input, list):
47
+ body["messages"] = input
48
+ else:
49
+ from morphsdk._errors import ValidationError
50
+
51
+ raise ValidationError("Either 'input' or 'messages' must be provided")
52
+
53
+ return body
54
+
55
+
56
+ def compact(
57
+ client: BaseClient,
58
+ *,
59
+ input: str | list[dict[str, Any]] | None = None,
60
+ messages: list[dict[str, Any]] | None = None,
61
+ query: str | None = None,
62
+ compression_ratio: float = 0.5,
63
+ preserve_recent: int = 2,
64
+ include_line_ranges: bool = True,
65
+ include_markers: bool = True,
66
+ model: str | None = None,
67
+ timeout: float | None = None,
68
+ ) -> CompactResult:
69
+ """Compact messages or text via ``/v1/compact``.
70
+
71
+ Accepts either a plain *input* string, a list of message dicts via
72
+ *input*, or a separate *messages* keyword. Returns per-message
73
+ ``compacted_line_ranges`` showing which lines were removed.
74
+
75
+ *include_line_ranges* controls whether the response includes
76
+ ``compacted_line_ranges`` metadata (default ``True``). *include_markers*
77
+ controls whether ``(filtered N lines)`` text markers are inserted; when
78
+ ``False``, removed gaps collapse to empty lines (default ``True``).
79
+ """
80
+ body = _build_compact_body(
81
+ input=input,
82
+ messages=messages,
83
+ query=query,
84
+ compression_ratio=compression_ratio,
85
+ preserve_recent=preserve_recent,
86
+ include_line_ranges=include_line_ranges,
87
+ include_markers=include_markers,
88
+ model=model,
89
+ )
90
+
91
+ response = client._request(
92
+ "POST",
93
+ f"{API_BASE_URL}/v1/compact",
94
+ json=body,
95
+ timeout=timeout or COMPACT_TIMEOUT,
96
+ )
97
+
98
+ return CompactResult.model_validate(response.json())
99
+
100
+
101
+ async def acompact(
102
+ client: AsyncBaseClient,
103
+ *,
104
+ input: str | list[dict[str, Any]] | None = None,
105
+ messages: list[dict[str, Any]] | None = None,
106
+ query: str | None = None,
107
+ compression_ratio: float = 0.5,
108
+ preserve_recent: int = 2,
109
+ include_line_ranges: bool = True,
110
+ include_markers: bool = True,
111
+ model: str | None = None,
112
+ timeout: float | None = None,
113
+ ) -> CompactResult:
114
+ """Async counterpart of :func:`compact`. Same wire contract."""
115
+ body = _build_compact_body(
116
+ input=input,
117
+ messages=messages,
118
+ query=query,
119
+ compression_ratio=compression_ratio,
120
+ preserve_recent=preserve_recent,
121
+ include_line_ranges=include_line_ranges,
122
+ include_markers=include_markers,
123
+ model=model,
124
+ )
125
+
126
+ response = await client._request(
127
+ "POST",
128
+ f"{API_BASE_URL}/v1/compact",
129
+ json=body,
130
+ timeout=timeout or COMPACT_TIMEOUT,
131
+ )
132
+
133
+ return CompactResult.model_validate(response.json())
@@ -0,0 +1,506 @@
1
+ """Fast Apply resource -- AI-powered code editing with intelligent merge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import difflib
6
+ import logging
7
+ import os
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from morphsdk._constants import API_BASE_URL, FAST_APPLY_MODEL_FAST, FAST_APPLY_MODEL_LARGE
12
+ from morphsdk.types.edit import ApplyEditResult, EditChanges, EditFileResult
13
+
14
+ if TYPE_CHECKING:
15
+ from morphsdk._base import AsyncBaseClient, BaseClient
16
+
17
+ logger = logging.getLogger("morphsdk")
18
+
19
+
20
+ def _generate_udiff(original: str, modified: str, filepath: str) -> str:
21
+ """Generate a unified diff between *original* and *modified*."""
22
+ return "".join(
23
+ difflib.unified_diff(
24
+ original.splitlines(keepends=True),
25
+ modified.splitlines(keepends=True),
26
+ fromfile=filepath,
27
+ tofile=filepath,
28
+ lineterm="",
29
+ )
30
+ )
31
+
32
+
33
+ def _count_changes(original: str, modified: str) -> EditChanges:
34
+ """Count added / removed / modified lines from a unified diff."""
35
+ diff = list(
36
+ difflib.unified_diff(
37
+ original.splitlines(keepends=True),
38
+ modified.splitlines(keepends=True),
39
+ lineterm="",
40
+ )
41
+ )
42
+
43
+ added = 0
44
+ removed = 0
45
+ for line in diff:
46
+ if line.startswith("+") and not line.startswith("+++"):
47
+ added += 1
48
+ elif line.startswith("-") and not line.startswith("---"):
49
+ removed += 1
50
+
51
+ modified_count = min(added, removed)
52
+ return EditChanges(
53
+ lines_added=added - modified_count,
54
+ lines_removed=removed - modified_count,
55
+ lines_modified=modified_count,
56
+ )
57
+
58
+
59
+ def _build_message(instruction: str, original_code: str, code_edit: str) -> str:
60
+ return (
61
+ f"<instruction>{instruction}</instruction>\n"
62
+ f"<code>{original_code}</code>\n"
63
+ f"<update>{code_edit}</update>"
64
+ )
65
+
66
+
67
+ def _format_edit_result(result: EditFileResult) -> str:
68
+ """Human-readable summary of an applied edit (shared by tool adapters)."""
69
+ c = result.changes
70
+ return (
71
+ f"Successfully applied changes to {result.path}. "
72
+ f"Added {c.lines_added}, removed {c.lines_removed}, modified {c.lines_modified}."
73
+ )
74
+
75
+
76
+ def _build_apply_body(
77
+ original_code: str, code_edit: str, instruction: str
78
+ ) -> dict[str, Any]:
79
+ """Build the ``/v1/chat/completions`` body for a Fast Apply merge."""
80
+ message = _build_message(instruction, original_code, code_edit)
81
+ model = (
82
+ FAST_APPLY_MODEL_LARGE
83
+ if os.environ.get("MORPH_LARGE_APPLY", "true") != "false"
84
+ else FAST_APPLY_MODEL_FAST
85
+ )
86
+ return {"model": model, "messages": [{"role": "user", "content": message}]}
87
+
88
+
89
+ def _parse_apply_response(data: dict[str, Any]) -> tuple[str, str | None]:
90
+ """Parse a Fast Apply chat-completions response into ``(merged_code, id)``."""
91
+ content = data["choices"][0]["message"]["content"]
92
+ if not content:
93
+ from morphsdk._errors import MorphError
94
+
95
+ raise MorphError("Morph API returned empty response")
96
+
97
+ return content, data.get("id")
98
+
99
+
100
+ def _resolve_edit_path(path: str, base_dir: str | None) -> str | None:
101
+ """Resolve *path* under *base_dir* (or cwd), guarding against escape.
102
+
103
+ Returns the normalized absolute path, or ``None`` if it escapes *base_dir*.
104
+ """
105
+ base = base_dir or os.getcwd()
106
+ full_path = os.path.normpath(os.path.join(base, path))
107
+ if not full_path.startswith(os.path.normpath(base)):
108
+ return None
109
+ return full_path
110
+
111
+
112
+ def _read_original(full_path: str) -> str:
113
+ """Read existing file content, or an empty string for new files."""
114
+ try:
115
+ return Path(full_path).read_text(encoding="utf-8")
116
+ except FileNotFoundError:
117
+ return ""
118
+
119
+
120
+ def _write_merged(full_path: str, merged_code: str) -> None:
121
+ """Write *merged_code* to *full_path*, creating parent directories."""
122
+ Path(full_path).parent.mkdir(parents=True, exist_ok=True)
123
+ Path(full_path).write_text(merged_code, encoding="utf-8")
124
+
125
+
126
+ def _assemble_file_result(
127
+ *,
128
+ path: str,
129
+ original_code: str,
130
+ merged_code: str,
131
+ completion_id: str | None,
132
+ generate_udiff: bool,
133
+ ) -> EditFileResult:
134
+ """Build the public :class:`EditFileResult` from a merge."""
135
+ udiff = _generate_udiff(original_code, merged_code, path) if generate_udiff else None
136
+ return EditFileResult(
137
+ path=path,
138
+ udiff=udiff,
139
+ changes=_count_changes(original_code, merged_code),
140
+ completion_id=completion_id,
141
+ )
142
+
143
+
144
+ def _assemble_code_result(
145
+ *,
146
+ original_code: str,
147
+ merged_code: str,
148
+ completion_id: str | None,
149
+ generate_udiff: bool,
150
+ ) -> ApplyEditResult:
151
+ """Build the public :class:`ApplyEditResult` from a merge."""
152
+ udiff = _generate_udiff(original_code, merged_code, "file") if generate_udiff else None
153
+ return ApplyEditResult(
154
+ merged_code=merged_code,
155
+ udiff=udiff,
156
+ changes=_count_changes(original_code, merged_code),
157
+ completion_id=completion_id,
158
+ )
159
+
160
+
161
+ class EditResource:
162
+ """AI-powered code editing via Morph Fast Apply."""
163
+
164
+ def __init__(self, client: BaseClient) -> None:
165
+ self._client = client
166
+
167
+ # ------------------------------------------------------------------
168
+ # file() -- read from disk, merge, write back
169
+ # ------------------------------------------------------------------
170
+
171
+ def file(
172
+ self,
173
+ *,
174
+ path: str,
175
+ instruction: str,
176
+ code_edit: str,
177
+ base_dir: str | None = None,
178
+ generate_udiff: bool = True,
179
+ auto_write: bool = True,
180
+ timeout: float | None = None,
181
+ ) -> EditFileResult:
182
+ """Edit a file on disk using Morph Fast Apply.
183
+
184
+ 1. Resolves *path* relative to *base_dir* (or cwd).
185
+ 2. Reads current content (empty string for new files).
186
+ 3. Calls the Morph merge API.
187
+ 4. Optionally writes the merged result and generates a udiff.
188
+ """
189
+ full_path = _resolve_edit_path(path, base_dir)
190
+ if full_path is None:
191
+ return EditFileResult(
192
+ path=path,
193
+ changes=EditChanges(lines_added=0, lines_removed=0, lines_modified=0),
194
+ )
195
+
196
+ original_code = _read_original(full_path)
197
+
198
+ merged_code, completion_id = self._call_api(
199
+ original_code, code_edit, instruction, timeout=timeout
200
+ )
201
+
202
+ if auto_write:
203
+ _write_merged(full_path, merged_code)
204
+
205
+ return _assemble_file_result(
206
+ path=path,
207
+ original_code=original_code,
208
+ merged_code=merged_code,
209
+ completion_id=completion_id,
210
+ generate_udiff=generate_udiff,
211
+ )
212
+
213
+ # ------------------------------------------------------------------
214
+ # code() -- pure code-in / code-out, no file I/O
215
+ # ------------------------------------------------------------------
216
+
217
+ def code(
218
+ self,
219
+ *,
220
+ original_code: str,
221
+ code_edit: str,
222
+ instruction: str,
223
+ generate_udiff: bool = True,
224
+ timeout: float | None = None,
225
+ ) -> ApplyEditResult:
226
+ """Apply an edit to code directly without file I/O."""
227
+ merged_code, completion_id = self._call_api(
228
+ original_code, code_edit, instruction, timeout=timeout
229
+ )
230
+
231
+ return _assemble_code_result(
232
+ original_code=original_code,
233
+ merged_code=merged_code,
234
+ completion_id=completion_id,
235
+ generate_udiff=generate_udiff,
236
+ )
237
+
238
+ # ------------------------------------------------------------------
239
+ # Internal
240
+ # ------------------------------------------------------------------
241
+
242
+ # ------------------------------------------------------------------
243
+ # Framework adapters
244
+ # ------------------------------------------------------------------
245
+
246
+ def as_openai_tool(
247
+ self,
248
+ *,
249
+ base_dir: str | None = None,
250
+ description: str | None = None,
251
+ ) -> Any:
252
+ """Create an OpenAI ChatCompletionTool for edit_file."""
253
+ from morphsdk.adapters.openai import (
254
+ EDIT_FILE_SYSTEM_PROMPT,
255
+ OpenAITool,
256
+ edit_file_tool_def,
257
+ )
258
+
259
+ effective_base_dir = base_dir
260
+
261
+ def executor(
262
+ target_filepath: str,
263
+ instruction: str,
264
+ code_edit: str,
265
+ ) -> EditFileResult:
266
+ return self.file(
267
+ path=target_filepath,
268
+ instruction=instruction,
269
+ code_edit=code_edit,
270
+ base_dir=effective_base_dir,
271
+ )
272
+
273
+ return OpenAITool(
274
+ definition=edit_file_tool_def(description=description),
275
+ executor=executor,
276
+ formatter=_format_edit_result,
277
+ system_prompt=EDIT_FILE_SYSTEM_PROMPT,
278
+ )
279
+
280
+ def as_anthropic_tool(
281
+ self,
282
+ *,
283
+ base_dir: str | None = None,
284
+ description: str | None = None,
285
+ ) -> Any:
286
+ """Create an Anthropic Tool for edit_file."""
287
+ from morphsdk.adapters.anthropic import AnthropicTool, edit_file_tool_def
288
+ from morphsdk.adapters.openai import EDIT_FILE_SYSTEM_PROMPT
289
+
290
+ effective_base_dir = base_dir
291
+
292
+ def executor(
293
+ target_filepath: str,
294
+ instruction: str,
295
+ code_edit: str,
296
+ ) -> EditFileResult:
297
+ return self.file(
298
+ path=target_filepath,
299
+ instruction=instruction,
300
+ code_edit=code_edit,
301
+ base_dir=effective_base_dir,
302
+ )
303
+
304
+ return AnthropicTool(
305
+ definition=edit_file_tool_def(description=description),
306
+ executor=executor,
307
+ formatter=_format_edit_result,
308
+ system_prompt=EDIT_FILE_SYSTEM_PROMPT,
309
+ )
310
+
311
+ # ------------------------------------------------------------------
312
+ # Internal
313
+ # ------------------------------------------------------------------
314
+
315
+ def _call_api(
316
+ self,
317
+ original_code: str,
318
+ code_edit: str,
319
+ instruction: str,
320
+ *,
321
+ timeout: float | None = None,
322
+ ) -> tuple[str, str | None]:
323
+ """Call the Morph Fast Apply chat completions endpoint.
324
+
325
+ Returns ``(merged_code, completion_id)``.
326
+ """
327
+ response = self._client._request(
328
+ "POST",
329
+ f"{API_BASE_URL}/v1/chat/completions",
330
+ json=_build_apply_body(original_code, code_edit, instruction),
331
+ timeout=timeout,
332
+ )
333
+
334
+ return _parse_apply_response(response.json())
335
+
336
+
337
+ class AsyncEditResource:
338
+ """Async AI-powered code editing via Morph Fast Apply."""
339
+
340
+ def __init__(self, client: AsyncBaseClient) -> None:
341
+ self._client = client
342
+
343
+ # ------------------------------------------------------------------
344
+ # file() -- read from disk, merge, write back
345
+ # ------------------------------------------------------------------
346
+
347
+ async def file(
348
+ self,
349
+ *,
350
+ path: str,
351
+ instruction: str,
352
+ code_edit: str,
353
+ base_dir: str | None = None,
354
+ generate_udiff: bool = True,
355
+ auto_write: bool = True,
356
+ timeout: float | None = None,
357
+ ) -> EditFileResult:
358
+ """Edit a file on disk using Morph Fast Apply.
359
+
360
+ 1. Resolves *path* relative to *base_dir* (or cwd).
361
+ 2. Reads current content (empty string for new files).
362
+ 3. Calls the Morph merge API.
363
+ 4. Optionally writes the merged result and generates a udiff.
364
+ """
365
+ full_path = _resolve_edit_path(path, base_dir)
366
+ if full_path is None:
367
+ return EditFileResult(
368
+ path=path,
369
+ changes=EditChanges(lines_added=0, lines_removed=0, lines_modified=0),
370
+ )
371
+
372
+ original_code = _read_original(full_path)
373
+
374
+ merged_code, completion_id = await self._call_api(
375
+ original_code, code_edit, instruction, timeout=timeout
376
+ )
377
+
378
+ if auto_write:
379
+ _write_merged(full_path, merged_code)
380
+
381
+ return _assemble_file_result(
382
+ path=path,
383
+ original_code=original_code,
384
+ merged_code=merged_code,
385
+ completion_id=completion_id,
386
+ generate_udiff=generate_udiff,
387
+ )
388
+
389
+ # ------------------------------------------------------------------
390
+ # code() -- pure code-in / code-out, no file I/O
391
+ # ------------------------------------------------------------------
392
+
393
+ async def code(
394
+ self,
395
+ *,
396
+ original_code: str,
397
+ code_edit: str,
398
+ instruction: str,
399
+ generate_udiff: bool = True,
400
+ timeout: float | None = None,
401
+ ) -> ApplyEditResult:
402
+ """Apply an edit to code directly without file I/O."""
403
+ merged_code, completion_id = await self._call_api(
404
+ original_code, code_edit, instruction, timeout=timeout
405
+ )
406
+
407
+ return _assemble_code_result(
408
+ original_code=original_code,
409
+ merged_code=merged_code,
410
+ completion_id=completion_id,
411
+ generate_udiff=generate_udiff,
412
+ )
413
+
414
+ # ------------------------------------------------------------------
415
+ # Framework adapters
416
+ # ------------------------------------------------------------------
417
+
418
+ def as_openai_tool(
419
+ self,
420
+ *,
421
+ base_dir: str | None = None,
422
+ description: str | None = None,
423
+ ) -> Any:
424
+ """Create an OpenAI ChatCompletionTool for edit_file with an async executor."""
425
+ from morphsdk.adapters.openai import (
426
+ EDIT_FILE_SYSTEM_PROMPT,
427
+ OpenAITool,
428
+ edit_file_tool_def,
429
+ )
430
+
431
+ effective_base_dir = base_dir
432
+
433
+ async def executor(
434
+ target_filepath: str,
435
+ instruction: str,
436
+ code_edit: str,
437
+ ) -> EditFileResult:
438
+ return await self.file(
439
+ path=target_filepath,
440
+ instruction=instruction,
441
+ code_edit=code_edit,
442
+ base_dir=effective_base_dir,
443
+ )
444
+
445
+ return OpenAITool(
446
+ definition=edit_file_tool_def(description=description),
447
+ executor=executor,
448
+ formatter=_format_edit_result,
449
+ system_prompt=EDIT_FILE_SYSTEM_PROMPT,
450
+ )
451
+
452
+ def as_anthropic_tool(
453
+ self,
454
+ *,
455
+ base_dir: str | None = None,
456
+ description: str | None = None,
457
+ ) -> Any:
458
+ """Create an Anthropic Tool for edit_file with an async executor."""
459
+ from morphsdk.adapters.anthropic import AnthropicTool, edit_file_tool_def
460
+ from morphsdk.adapters.openai import EDIT_FILE_SYSTEM_PROMPT
461
+
462
+ effective_base_dir = base_dir
463
+
464
+ async def executor(
465
+ target_filepath: str,
466
+ instruction: str,
467
+ code_edit: str,
468
+ ) -> EditFileResult:
469
+ return await self.file(
470
+ path=target_filepath,
471
+ instruction=instruction,
472
+ code_edit=code_edit,
473
+ base_dir=effective_base_dir,
474
+ )
475
+
476
+ return AnthropicTool(
477
+ definition=edit_file_tool_def(description=description),
478
+ executor=executor,
479
+ formatter=_format_edit_result,
480
+ system_prompt=EDIT_FILE_SYSTEM_PROMPT,
481
+ )
482
+
483
+ # ------------------------------------------------------------------
484
+ # Internal
485
+ # ------------------------------------------------------------------
486
+
487
+ async def _call_api(
488
+ self,
489
+ original_code: str,
490
+ code_edit: str,
491
+ instruction: str,
492
+ *,
493
+ timeout: float | None = None,
494
+ ) -> tuple[str, str | None]:
495
+ """Call the Morph Fast Apply chat completions endpoint.
496
+
497
+ Returns ``(merged_code, completion_id)``.
498
+ """
499
+ response = await self._client._request(
500
+ "POST",
501
+ f"{API_BASE_URL}/v1/chat/completions",
502
+ json=_build_apply_body(original_code, code_edit, instruction),
503
+ timeout=timeout,
504
+ )
505
+
506
+ return _parse_apply_response(response.json())