lmcoding-local 3.2.0__tar.gz → 3.2.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.
Files changed (24) hide show
  1. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/PKG-INFO +9 -4
  2. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/README.md +8 -3
  3. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/pyproject.toml +1 -1
  4. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/__init__.py +1 -1
  5. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/agent.py +215 -24
  6. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding_local.egg-info/PKG-INFO +9 -4
  7. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/tests/test_core.py +29 -0
  8. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/LICENSE +0 -0
  9. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/setup.cfg +0 -0
  10. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/__main__.py +0 -0
  11. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/checkpoints.py +0 -0
  12. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/cli.py +0 -0
  13. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/config.py +0 -0
  14. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/permissions.py +0 -0
  15. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/sessions.py +0 -0
  16. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/tools.py +0 -0
  17. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/ui.py +0 -0
  18. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/verifier.py +0 -0
  19. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding/workspace.py +0 -0
  20. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding_local.egg-info/SOURCES.txt +0 -0
  21. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding_local.egg-info/dependency_links.txt +0 -0
  22. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding_local.egg-info/entry_points.txt +0 -0
  23. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding_local.egg-info/requires.txt +0 -0
  24. {lmcoding_local-3.2.0 → lmcoding_local-3.2.1}/src/lmcoding_local.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lmcoding-local
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: Agente de programación local para LM Studio con interfaz tipo Codex y seguridad por workspace
5
5
  Author: TV Ofertas Ecuador
6
6
  License-Expression: MIT
@@ -21,7 +21,7 @@ Requires-Dist: openai>=1.40.0
21
21
  Requires-Dist: rich>=13.7.0
22
22
  Dynamic: license-file
23
23
 
24
- # llmCodex 3.2.0
24
+ # llmCodex 3.2.1
25
25
 
26
26
  Agente de programación local para **LM Studio**, con experiencia de terminal inspirada en agentes modernos tipo Codex, colores violeta propios y herramientas para inspeccionar, editar, verificar y reparar proyectos.
27
27
 
@@ -94,13 +94,13 @@ Y agrega al PATH:
94
94
  ## Instalar el wheel manualmente
95
95
 
96
96
  ```powershell
97
- py -m pip install dist\lmcoding_local-3.2.0-py3-none-any.whl
97
+ py -m pip install dist\lmcoding_local-3.2.1-py3-none-any.whl
98
98
  ```
99
99
 
100
100
  Para aplicaciones CLI es recomendable usar `pipx`:
101
101
 
102
102
  ```powershell
103
- pipx install dist\lmcoding_local-3.2.0-py3-none-any.whl
103
+ pipx install dist\lmcoding_local-3.2.1-py3-none-any.whl
104
104
  ```
105
105
 
106
106
  ## Preparar LM Studio
@@ -342,3 +342,8 @@ Y selecciona **Build → Compile**.
342
342
  ## Límites reales
343
343
 
344
344
  El sistema puede detectar, probar, corregir y revertir muchos errores, pero ningún agente puede garantizar reparar absolutamente cualquier bug. Puede necesitar credenciales, servicios externos, hardware, datos privados o una especificación más precisa. llmCodex evita declarar éxito cuando sus verificaciones siguen fallando y puede restaurar el checkpoint anterior.
345
+
346
+
347
+ ## Corrección 3.2.1
348
+
349
+ Incluye un parser de compatibilidad para modelos que devuelven llamadas de herramientas como texto en lugar de `tool_calls` estructurados.
@@ -1,4 +1,4 @@
1
- # llmCodex 3.2.0
1
+ # llmCodex 3.2.1
2
2
 
3
3
  Agente de programación local para **LM Studio**, con experiencia de terminal inspirada en agentes modernos tipo Codex, colores violeta propios y herramientas para inspeccionar, editar, verificar y reparar proyectos.
4
4
 
@@ -71,13 +71,13 @@ Y agrega al PATH:
71
71
  ## Instalar el wheel manualmente
72
72
 
73
73
  ```powershell
74
- py -m pip install dist\lmcoding_local-3.2.0-py3-none-any.whl
74
+ py -m pip install dist\lmcoding_local-3.2.1-py3-none-any.whl
75
75
  ```
76
76
 
77
77
  Para aplicaciones CLI es recomendable usar `pipx`:
78
78
 
79
79
  ```powershell
80
- pipx install dist\lmcoding_local-3.2.0-py3-none-any.whl
80
+ pipx install dist\lmcoding_local-3.2.1-py3-none-any.whl
81
81
  ```
82
82
 
83
83
  ## Preparar LM Studio
@@ -319,3 +319,8 @@ Y selecciona **Build → Compile**.
319
319
  ## Límites reales
320
320
 
321
321
  El sistema puede detectar, probar, corregir y revertir muchos errores, pero ningún agente puede garantizar reparar absolutamente cualquier bug. Puede necesitar credenciales, servicios externos, hardware, datos privados o una especificación más precisa. llmCodex evita declarar éxito cuando sus verificaciones siguen fallando y puede restaurar el checkpoint anterior.
322
+
323
+
324
+ ## Corrección 3.2.1
325
+
326
+ Incluye un parser de compatibilidad para modelos que devuelven llamadas de herramientas como texto en lugar de `tool_calls` estructurados.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lmcoding-local"
7
- version = "3.2.0"
7
+ version = "3.2.1"
8
8
  description = "Agente de programación local para LM Studio con interfaz tipo Codex y seguridad por workspace"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """llmCodex / LMCoding package."""
2
2
 
3
- __version__ = "3.2.0"
3
+ __version__ = "3.2.1"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import re
5
5
  import time
6
+ import uuid
6
7
  from collections import Counter
7
8
  from typing import Any
8
9
 
@@ -29,6 +30,8 @@ Reglas obligatorias:
29
30
  8. No declares que está corregido mientras las verificaciones sigan fallando.
30
31
  9. Resume archivos modificados, pruebas ejecutadas y problemas pendientes.
31
32
  10. Responde en el idioma del usuario y no reveles razonamiento privado paso a paso.
33
+ 11. Solicita herramientas usando únicamente sus nombres exactos, sin prefijos como file_manager.
34
+ 12. Nunca muestres etiquetas internas como <|tool_call>, <tool_call> o [TOOL_REQUEST] al usuario.
32
35
  """
33
36
 
34
37
 
@@ -96,23 +99,189 @@ class LMCodingAgent:
96
99
  self.session.save()
97
100
 
98
101
  @staticmethod
99
- def _repair_json(raw: str) -> dict[str, Any]:
100
- text = raw.strip()
102
+ def _split_top_level(text: str, delimiter: str = ",") -> list[str]:
103
+ parts: list[str] = []
104
+ start = 0
105
+ depth = 0
106
+ quote: str | None = None
107
+ escaped = False
108
+ for index, char in enumerate(text):
109
+ if quote is not None:
110
+ if escaped:
111
+ escaped = False
112
+ elif char == "\\":
113
+ escaped = True
114
+ elif char == quote:
115
+ quote = None
116
+ continue
117
+ if char in {"\"", "'"}:
118
+ quote = char
119
+ elif char in "[{(":
120
+ depth += 1
121
+ elif char in "]})":
122
+ depth = max(0, depth - 1)
123
+ elif char == delimiter and depth == 0:
124
+ parts.append(text[start:index].strip())
125
+ start = index + 1
126
+ tail = text[start:].strip()
127
+ if tail:
128
+ parts.append(tail)
129
+ return parts
130
+
131
+ @classmethod
132
+ def _parse_loose_value(cls, raw: str) -> Any:
133
+ value = raw.strip()
134
+ value = value.replace('<|"|>', '"').replace("<|'|>", "'")
135
+ if not value:
136
+ return ""
137
+ if value[0:1] in {"\"", "'"} and value[-1:] == value[0]:
138
+ # Preserve Windows paths such as C:\\Users\\name instead of treating
139
+ # backslashes as JSON escape sequences.
140
+ inner = value[1:-1]
141
+ return inner.replace(r'\"', '"').replace(r"\'", "'")
142
+ if value.startswith("{") and value.endswith("}"):
143
+ result: dict[str, Any] = {}
144
+ body = value[1:-1].strip()
145
+ if not body:
146
+ return result
147
+ for item in cls._split_top_level(body):
148
+ key_value = cls._split_key_value(item)
149
+ if key_value is None:
150
+ continue
151
+ key, item_value = key_value
152
+ result[key] = cls._parse_loose_value(item_value)
153
+ return result
154
+ if value.startswith("[") and value.endswith("]"):
155
+ body = value[1:-1].strip()
156
+ return [] if not body else [cls._parse_loose_value(item) for item in cls._split_top_level(body)]
157
+ lowered = value.lower()
158
+ if lowered == "true":
159
+ return True
160
+ if lowered == "false":
161
+ return False
162
+ if lowered in {"null", "none"}:
163
+ return None
164
+ try:
165
+ return int(value)
166
+ except ValueError:
167
+ try:
168
+ return float(value)
169
+ except ValueError:
170
+ return value
171
+
172
+ @staticmethod
173
+ def _split_key_value(item: str) -> tuple[str, str] | None:
174
+ depth = 0
175
+ quote: str | None = None
176
+ escaped = False
177
+ for index, char in enumerate(item):
178
+ if quote is not None:
179
+ if escaped:
180
+ escaped = False
181
+ elif char == "\\":
182
+ escaped = True
183
+ elif char == quote:
184
+ quote = None
185
+ continue
186
+ if char in {"\"", "'"}:
187
+ quote = char
188
+ elif char in "[{(":
189
+ depth += 1
190
+ elif char in "]})":
191
+ depth = max(0, depth - 1)
192
+ elif char == ":" and depth == 0:
193
+ key = item[:index].strip().strip("\"'")
194
+ return key, item[index + 1:].strip()
195
+ return None
196
+
197
+ @classmethod
198
+ def _repair_json(cls, raw: str) -> dict[str, Any]:
199
+ text = (raw or "{}").strip()
200
+ text = text.replace('<|"|>', '"').replace("<|'|>", "'")
101
201
  try:
102
202
  value = json.loads(text or "{}")
103
203
  return value if isinstance(value, dict) else {}
104
- except json.JSONDecodeError:
204
+ except (json.JSONDecodeError, TypeError):
105
205
  pass
106
206
  match = re.search(r"\{.*\}", text, re.DOTALL)
107
- if match:
108
- candidate = match.group(0)
109
- candidate = re.sub(r",\s*([}\]])", r"\1", candidate)
110
- try:
111
- value = json.loads(candidate)
112
- return value if isinstance(value, dict) else {}
113
- except json.JSONDecodeError:
114
- return {}
115
- return {}
207
+ if not match:
208
+ return {}
209
+ candidate = match.group(0)
210
+ value = cls._parse_loose_value(candidate)
211
+ return value if isinstance(value, dict) else {}
212
+
213
+ def _canonical_tool_name(self, raw_name: str) -> str:
214
+ name = raw_name.strip().removeprefix("call:")
215
+ aliases = {
216
+ "file_manager.apply_patch": "apply_edits",
217
+ "apply_patch": "apply_edits",
218
+ "patch_file": "apply_edits",
219
+ "terminal.exec": "run_command",
220
+ "terminal.run": "run_command",
221
+ "shell.exec": "run_command",
222
+ "shell.run": "run_command",
223
+ }
224
+ if name in aliases:
225
+ return aliases[name]
226
+ if name in self.toolbox.functions:
227
+ return name
228
+ tail = re.split(r"[.:/]", name)[-1]
229
+ return aliases.get(tail, tail)
230
+
231
+ def _parse_tool_block(self, block: str, call_id: str) -> dict[str, Any] | None:
232
+ cleaned = block.strip()
233
+ cleaned = cleaned.replace('<|"|>', '"').replace("<|'|>", "'")
234
+
235
+ # Format emitted by some local models:
236
+ # call:file_manager.list_files{path:"C:\\project"}
237
+ direct = re.match(r"(?:call:)?(?P<name>[A-Za-z_][\w.:-]*)\s*(?P<args>\{.*\})\s*$", cleaned, re.DOTALL)
238
+ if direct:
239
+ name = self._canonical_tool_name(direct.group("name"))
240
+ arguments = self._repair_json(direct.group("args"))
241
+ return {"id": call_id, "name": name, "arguments": arguments}
242
+
243
+ payload = self._repair_json(cleaned)
244
+ if not payload:
245
+ return None
246
+ function_payload = payload.get("function") if isinstance(payload.get("function"), dict) else {}
247
+ raw_name = payload.get("name") or function_payload.get("name")
248
+ if not isinstance(raw_name, str):
249
+ return None
250
+ raw_arguments = payload.get("arguments", function_payload.get("arguments", {}))
251
+ if isinstance(raw_arguments, str):
252
+ arguments = self._repair_json(raw_arguments)
253
+ elif isinstance(raw_arguments, dict):
254
+ arguments = raw_arguments
255
+ else:
256
+ arguments = {}
257
+ return {
258
+ "id": call_id,
259
+ "name": self._canonical_tool_name(raw_name),
260
+ "arguments": arguments,
261
+ }
262
+
263
+ def _extract_text_tool_calls(self, content: str, step: int) -> tuple[list[dict[str, Any]], str]:
264
+ if not content:
265
+ return [], content
266
+ patterns = [
267
+ re.compile(r"<\|tool_call>(.*?)<tool_call\|>", re.DOTALL | re.IGNORECASE),
268
+ re.compile(r"<tool_call>(.*?)</tool_call>", re.DOTALL | re.IGNORECASE),
269
+ re.compile(r"\[TOOL_REQUEST\](.*?)\[END_TOOL_REQUEST\]", re.DOTALL | re.IGNORECASE),
270
+ ]
271
+ calls: list[dict[str, Any]] = []
272
+ visible = content
273
+ for pattern in patterns:
274
+ matches = list(pattern.finditer(visible))
275
+ for match in matches:
276
+ call = self._parse_tool_block(
277
+ match.group(1),
278
+ f"fallback_{step}_{len(calls) + 1}_{uuid.uuid4().hex[:8]}",
279
+ )
280
+ if call is not None:
281
+ calls.append(call)
282
+ if matches:
283
+ visible = pattern.sub("", visible)
284
+ return calls, visible.strip()
116
285
 
117
286
  def _completion(self):
118
287
  last_error: Exception | None = None
@@ -153,40 +322,62 @@ class LMCodingAgent:
153
322
  with console.status(f"[brand]llmCodex trabajando · paso {step}[/brand]", spinner="dots"):
154
323
  response = self._completion()
155
324
  message = response.choices[0].message
156
- tool_calls = message.tool_calls or []
157
- assistant_payload: dict[str, Any] = {"role": "assistant", "content": message.content or ""}
158
- if tool_calls:
325
+ content = message.content or ""
326
+ normalized_calls: list[dict[str, Any]] = []
327
+
328
+ for call in message.tool_calls or []:
329
+ normalized_calls.append({
330
+ "id": call.id or f"structured_{step}_{uuid.uuid4().hex[:8]}",
331
+ "name": self._canonical_tool_name(call.function.name),
332
+ "arguments": self._repair_json(call.function.arguments or "{}"),
333
+ })
334
+
335
+ # LM Studio documents that malformed calls can fall back into
336
+ # message.content. Recover common native/default formats here.
337
+ if not normalized_calls and content:
338
+ normalized_calls, content = self._extract_text_tool_calls(content, step)
339
+
340
+ assistant_payload: dict[str, Any] = {"role": "assistant", "content": content}
341
+ if normalized_calls:
159
342
  assistant_payload["tool_calls"] = [
160
343
  {
161
- "id": call.id,
344
+ "id": call["id"],
162
345
  "type": "function",
163
- "function": {"name": call.function.name, "arguments": call.function.arguments},
346
+ "function": {
347
+ "name": call["name"],
348
+ "arguments": json.dumps(call["arguments"], ensure_ascii=False),
349
+ },
164
350
  }
165
- for call in tool_calls
351
+ for call in normalized_calls
166
352
  ]
167
353
  self.session.messages.append(assistant_payload)
168
354
 
169
- if not tool_calls:
170
- final_text = message.content or "(sin respuesta)"
355
+ if not normalized_calls:
356
+ final_text = content or "(sin respuesta)"
171
357
  if render:
172
358
  console.print(Panel(Markdown(final_text), title="[brand]llmCodex[/brand]", border_style="magenta"))
173
359
  self.session.save()
174
360
  return final_text
175
361
 
176
- for call in tool_calls:
177
- name = call.function.name
178
- arguments = self._repair_json(call.function.arguments or "{}")
362
+ for call in normalized_calls:
363
+ name = call["name"]
364
+ arguments = call["arguments"]
179
365
  signature = f"{name}:{json.dumps(arguments, sort_keys=True, ensure_ascii=False)}"
180
366
  self.call_signatures[signature] += 1
181
367
  if self.call_signatures[signature] >= 4:
182
368
  result = "ERROR: bucle detectado; esta llamada idéntica ya se repitió varias veces. Cambia de estrategia."
369
+ elif name not in self.toolbox.functions:
370
+ result = (
371
+ f"ERROR: herramienta desconocida: {name}. "
372
+ f"Herramientas válidas: {', '.join(sorted(self.toolbox.functions))}"
373
+ )
183
374
  else:
184
375
  if render:
185
376
  console.print(f"[accent]› {name}[/accent] [muted]{truncate(json.dumps(arguments, ensure_ascii=False), 700)}[/muted]")
186
377
  result = self.toolbox.execute(name, arguments)
187
378
  if render:
188
379
  console.print(Panel(truncate(result, 3500), border_style="grey35", title="resultado"))
189
- self.session.messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
380
+ self.session.messages.append({"role": "tool", "tool_call_id": call["id"], "content": result})
190
381
 
191
382
  final_text = "Se alcanzó el máximo de pasos. Revisa el estado y pide continuar."
192
383
  self.session.save()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lmcoding-local
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: Agente de programación local para LM Studio con interfaz tipo Codex y seguridad por workspace
5
5
  Author: TV Ofertas Ecuador
6
6
  License-Expression: MIT
@@ -21,7 +21,7 @@ Requires-Dist: openai>=1.40.0
21
21
  Requires-Dist: rich>=13.7.0
22
22
  Dynamic: license-file
23
23
 
24
- # llmCodex 3.2.0
24
+ # llmCodex 3.2.1
25
25
 
26
26
  Agente de programación local para **LM Studio**, con experiencia de terminal inspirada en agentes modernos tipo Codex, colores violeta propios y herramientas para inspeccionar, editar, verificar y reparar proyectos.
27
27
 
@@ -94,13 +94,13 @@ Y agrega al PATH:
94
94
  ## Instalar el wheel manualmente
95
95
 
96
96
  ```powershell
97
- py -m pip install dist\lmcoding_local-3.2.0-py3-none-any.whl
97
+ py -m pip install dist\lmcoding_local-3.2.1-py3-none-any.whl
98
98
  ```
99
99
 
100
100
  Para aplicaciones CLI es recomendable usar `pipx`:
101
101
 
102
102
  ```powershell
103
- pipx install dist\lmcoding_local-3.2.0-py3-none-any.whl
103
+ pipx install dist\lmcoding_local-3.2.1-py3-none-any.whl
104
104
  ```
105
105
 
106
106
  ## Preparar LM Studio
@@ -342,3 +342,8 @@ Y selecciona **Build → Compile**.
342
342
  ## Límites reales
343
343
 
344
344
  El sistema puede detectar, probar, corregir y revertir muchos errores, pero ningún agente puede garantizar reparar absolutamente cualquier bug. Puede necesitar credenciales, servicios externos, hardware, datos privados o una especificación más precisa. llmCodex evita declarar éxito cuando sus verificaciones siguen fallando y puede restaurar el checkpoint anterior.
345
+
346
+
347
+ ## Corrección 3.2.1
348
+
349
+ Incluye un parser de compatibilidad para modelos que devuelven llamadas de herramientas como texto en lugar de `tool_calls` estructurados.
@@ -4,6 +4,7 @@ import tempfile
4
4
  import unittest
5
5
  from pathlib import Path
6
6
 
7
+ from lmcoding.agent import LMCodingAgent
7
8
  from lmcoding.checkpoints import CheckpointManager
8
9
  from lmcoding.permissions import ApprovalPolicy, PermissionMode, Risk, classify_command
9
10
  from lmcoding.tools import ToolBox
@@ -89,5 +90,33 @@ class VerifierTests(unittest.TestCase):
89
90
  self.assertTrue(verifier.verify("quick").ok)
90
91
 
91
92
 
93
+ class ToolCallFallbackTests(unittest.TestCase):
94
+ def make_agent(self):
95
+ agent = object.__new__(LMCodingAgent)
96
+ agent.toolbox = type("FakeToolbox", (), {"functions": {"list_files": lambda: None, "run_command": lambda: None, "apply_edits": lambda: None}})()
97
+ return agent
98
+
99
+ def test_recovers_reported_lmstudio_format(self):
100
+ agent = self.make_agent()
101
+ content = r'<|tool_call>call:file_manager.list_files{path:<|"|>C:\Users\tyler\Downloads\GemAI<|"|>}<tool_call|>'
102
+ calls, visible = agent._extract_text_tool_calls(content, 1)
103
+ self.assertEqual(visible, "")
104
+ self.assertEqual(len(calls), 1)
105
+ self.assertEqual(calls[0]["name"], "list_files")
106
+ self.assertEqual(calls[0]["arguments"]["path"], r"C:\Users\tyler\Downloads\GemAI")
107
+
108
+ def test_recovers_lmstudio_default_tool_request(self):
109
+ agent = self.make_agent()
110
+ content = '[TOOL_REQUEST]{"name":"run_command","arguments":{"command":"python -m pytest"}}[END_TOOL_REQUEST]'
111
+ calls, visible = agent._extract_text_tool_calls(content, 2)
112
+ self.assertEqual(visible, "")
113
+ self.assertEqual(calls[0]["name"], "run_command")
114
+ self.assertEqual(calls[0]["arguments"]["command"], "python -m pytest")
115
+
116
+ def test_aliases_apply_patch(self):
117
+ agent = self.make_agent()
118
+ self.assertEqual(agent._canonical_tool_name("file_manager.apply_patch"), "apply_edits")
119
+
120
+
92
121
  if __name__ == "__main__":
93
122
  unittest.main()
File without changes
File without changes