abstractcore 2.6.6__py3-none-any.whl → 2.6.8__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.
@@ -638,17 +638,26 @@
638
638
  "max_tokens": 262144
639
639
  },
640
640
  "qwen3-coder-30b": {
641
- "max_output_tokens": 8192,
641
+ "max_output_tokens": 65536,
642
642
  "tool_support": "native",
643
643
  "structured_output": "native",
644
644
  "parallel_tools": true,
645
645
  "vision_support": false,
646
646
  "audio_support": false,
647
- "notes": "Code-focused model with native tool support via chatml-function-calling format",
648
- "source": "Alibaba official docs",
647
+ "architecture": "mixture_of_experts",
648
+ "total_parameters": "30.5B",
649
+ "active_parameters": "3.3B",
650
+ "experts": 128,
651
+ "experts_activated": 8,
652
+ "notes": "Code-focused MoE model (30.5B total/3.3B active, 128 experts/8 activated). Native tool support via chatml-function-calling format. Supports up to 1M tokens with YaRN extension.",
653
+ "source": "Qwen HuggingFace model card 2025",
649
654
  "canonical_name": "qwen3-coder-30b",
650
- "aliases": [],
651
- "max_tokens": 32768
655
+ "aliases": [
656
+ "Qwen/Qwen3-Coder-30B-A3B-Instruct",
657
+ "qwen3-coder-30b-a3b",
658
+ "qwen3-coder-30b-a3b-instruct"
659
+ ],
660
+ "max_tokens": 262144
652
661
  },
653
662
  "qwen2-vl": {
654
663
  "max_output_tokens": 8192,
@@ -3,20 +3,13 @@ Image scaling utility for AbstractCore media handling.
3
3
 
4
4
  Provides intelligent image scaling based on model-specific requirements
5
5
  and capabilities for vision models.
6
-
7
- Requires: PIL (Pillow) - install with `pip install Pillow`
8
6
  """
9
7
 
10
8
  from typing import Tuple, Optional, Union, Dict, Any
11
9
  from enum import Enum
12
10
  from pathlib import Path
13
11
 
14
- try:
15
- from PIL import Image, ImageOps
16
- except ImportError as e:
17
- raise ImportError(
18
- "PIL (Pillow) is required for image scaling. Install with: pip install Pillow"
19
- ) from e
12
+ from PIL import Image, ImageOps
20
13
 
21
14
  from ..base import MediaProcessingError
22
15
  from ...utils.structured_logging import get_logger
@@ -5,14 +5,14 @@ Basic text processing capabilities built on top of AbstractCore,
5
5
  demonstrating how to leverage the core infrastructure for real-world tasks.
6
6
  """
7
7
 
8
- from .basic_summarizer import BasicSummarizer, SummaryStyle, SummaryLength
8
+ from .basic_summarizer import BasicSummarizer, SummaryStyle, SummaryLength, CompressionMode
9
9
  from .basic_extractor import BasicExtractor
10
10
  from .basic_judge import BasicJudge, JudgmentCriteria, Assessment, create_judge
11
11
  from .basic_deepsearch import BasicDeepSearch, ResearchReport, ResearchFinding, ResearchPlan, ResearchSubTask
12
12
  from .basic_intent import BasicIntentAnalyzer, IntentType, IntentDepth, IntentContext, IdentifiedIntent, IntentAnalysisOutput
13
13
 
14
14
  __all__ = [
15
- 'BasicSummarizer', 'SummaryStyle', 'SummaryLength',
15
+ 'BasicSummarizer', 'SummaryStyle', 'SummaryLength', 'CompressionMode',
16
16
  'BasicExtractor',
17
17
  'BasicJudge', 'JudgmentCriteria', 'Assessment', 'create_judge',
18
18
  'BasicDeepSearch', 'ResearchReport', 'ResearchFinding', 'ResearchPlan', 'ResearchSubTask',
@@ -35,6 +35,42 @@ class SummaryLength(Enum):
35
35
  COMPREHENSIVE = "comprehensive" # Full analysis with context
36
36
 
37
37
 
38
+ class CompressionMode(Enum):
39
+ """Compression aggressiveness for chat history summarization.
40
+
41
+ Controls how aggressively the summarizer compresses conversation history:
42
+ - LIGHT: Keep most information, only remove redundancy
43
+ - STANDARD: Balanced compression, main points and context
44
+ - HEAVY: Aggressive compression, only critical information
45
+ """
46
+ LIGHT = "light"
47
+ STANDARD = "standard"
48
+ HEAVY = "heavy"
49
+
50
+
51
+ # Compression mode-specific instructions for summarization prompts
52
+ COMPRESSION_INSTRUCTIONS = {
53
+ CompressionMode.LIGHT: (
54
+ "Preserve most details from this conversation while removing only redundancy. "
55
+ "Keep: all key decisions and outcomes, important context and background, "
56
+ "specific details/names/numbers/technical terms, all tool calls and results, "
57
+ "error messages and resolutions. Remove only: repetitive greetings, duplicate information."
58
+ ),
59
+ CompressionMode.STANDARD: (
60
+ "Summarize with balanced compression, keeping main points and essential context. "
61
+ "Keep: key decisions and rationale, important outcomes, critical context for ongoing work, "
62
+ "unresolved items and pending tasks. Remove: intermediate reasoning steps, "
63
+ "exploratory tangents, detailed tool outputs (keep only key findings)."
64
+ ),
65
+ CompressionMode.HEAVY: (
66
+ "Extract only the most critical information. Keep ONLY: final decisions made, "
67
+ "critical outcomes (success/failure), essential context to continue work, "
68
+ "blocking issues and hard dependencies. Remove: all exploratory discussion, "
69
+ "all intermediate steps, all detailed outputs, all background explanations."
70
+ ),
71
+ }
72
+
73
+
38
74
  class LLMSummaryOutput(BaseModel):
39
75
  """LLM-generated summary output (without word counts)"""
40
76
  summary: str = Field(description="The main summary text")
@@ -493,7 +529,8 @@ Create a unified summary that represents the entire document effectively."""
493
529
  self,
494
530
  messages: List[dict],
495
531
  preserve_recent: int = 6,
496
- focus: Optional[str] = None
532
+ focus: Optional[str] = None,
533
+ compression_mode: CompressionMode = CompressionMode.STANDARD
497
534
  ) -> SummaryOutput:
498
535
  """
499
536
  Specialized method for chat history summarization following SOTA 2025 practices
@@ -502,6 +539,7 @@ Create a unified summary that represents the entire document effectively."""
502
539
  messages: List of message dicts with 'role' and 'content' keys
503
540
  preserve_recent: Number of recent messages to keep intact (default 6)
504
541
  focus: Optional focus for summarization (e.g., "key decisions", "technical solutions")
542
+ compression_mode: How aggressively to compress (LIGHT, STANDARD, HEAVY)
505
543
 
506
544
  Returns:
507
545
  SummaryOutput: Structured summary optimized for chat history context
@@ -511,36 +549,67 @@ Create a unified summary that represents the entire document effectively."""
511
549
  - Focuses on decisions, solutions, and ongoing topics
512
550
  - Maintains user intent and assistant responses
513
551
  - Optimized for chat continuation rather than standalone summary
552
+
553
+ Compression Modes:
554
+ - LIGHT: Keep most information, only remove redundancy
555
+ - STANDARD: Balanced compression, main points and context
556
+ - HEAVY: Aggressive compression, only critical information
514
557
  """
558
+ # Build focus with compression instructions
559
+ compression_instruction = COMPRESSION_INSTRUCTIONS.get(
560
+ compression_mode,
561
+ COMPRESSION_INSTRUCTIONS[CompressionMode.STANDARD]
562
+ )
563
+
564
+ # Combine user focus with compression instruction
565
+ if focus:
566
+ effective_focus = f"{compression_instruction} Focus especially on: {focus}"
567
+ else:
568
+ effective_focus = compression_instruction
569
+
570
+ # Map compression mode to summary length for appropriate output size
571
+ length_map = {
572
+ CompressionMode.LIGHT: SummaryLength.DETAILED,
573
+ CompressionMode.STANDARD: SummaryLength.STANDARD,
574
+ CompressionMode.HEAVY: SummaryLength.BRIEF,
575
+ }
576
+ target_length = length_map.get(compression_mode, SummaryLength.STANDARD)
577
+
578
+ logger.debug("Chat history summarization with compression mode",
579
+ message_count=len(messages),
580
+ preserve_recent=preserve_recent,
581
+ compression_mode=compression_mode.value,
582
+ target_length=target_length.value)
583
+
515
584
  if len(messages) <= preserve_recent:
516
585
  # If short enough, just summarize normally
517
- logger.debug("Chat history is short, using standard summarization",
518
- message_count=len(messages),
586
+ logger.debug("Chat history is short, using standard summarization",
587
+ message_count=len(messages),
519
588
  preserve_recent=preserve_recent)
520
589
  chat_text = self._format_chat_messages_to_text(messages)
521
590
  return self.summarize(
522
591
  chat_text,
523
- focus=focus or "conversational context and key information",
592
+ focus=effective_focus,
524
593
  style=SummaryStyle.CONVERSATIONAL,
525
- length=SummaryLength.STANDARD
594
+ length=target_length
526
595
  )
527
596
 
528
597
  # Split into older messages (to summarize) and recent messages (to preserve)
529
598
  older_messages = messages[:-preserve_recent]
530
599
  recent_messages = messages[-preserve_recent:]
531
-
532
- logger.debug("Splitting chat history for summarization",
600
+
601
+ logger.debug("Splitting chat history for summarization",
533
602
  total_messages=len(messages),
534
603
  older_messages=len(older_messages),
535
604
  recent_messages=len(recent_messages))
536
605
 
537
- # Summarize older messages with conversational focus
606
+ # Summarize older messages with conversational focus and compression mode
538
607
  older_text = self._format_chat_messages_to_text(older_messages)
539
608
  older_summary = self.summarize(
540
609
  older_text,
541
- focus=focus or "key decisions, solutions, and ongoing context",
610
+ focus=effective_focus,
542
611
  style=SummaryStyle.CONVERSATIONAL,
543
- length=SummaryLength.DETAILED
612
+ length=target_length
544
613
  )
545
614
 
546
615
  # The summary should ONLY contain the older messages summary
@@ -5,6 +5,7 @@ Base provider with integrated telemetry, events, and exception handling.
5
5
  import time
6
6
  import uuid
7
7
  import asyncio
8
+ import warnings
8
9
  from collections import deque
9
10
  from typing import List, Dict, Any, Optional, Union, Iterator, AsyncIterator, Type
10
11
  from abc import ABC, abstractmethod
@@ -60,6 +61,13 @@ class BaseProvider(AbstractCoreInterface, ABC):
60
61
  # execute_tools: True = AbstractCore executes tools (legacy mode)
61
62
  # False = Pass-through mode (default - for API server / agentic CLI)
62
63
  self.execute_tools = kwargs.get('execute_tools', False)
64
+ if self.execute_tools:
65
+ warnings.warn(
66
+ "execute_tools=True is deprecated. Prefer passing tools explicitly to generate() "
67
+ "and executing tool calls in the host/runtime via a ToolExecutor.",
68
+ DeprecationWarning,
69
+ stacklevel=2,
70
+ )
63
71
 
64
72
  # Setup retry manager with optional configuration
65
73
  retry_config = kwargs.get('retry_config', None)
@@ -202,6 +210,12 @@ class BaseProvider(AbstractCoreInterface, ABC):
202
210
  """
203
211
  trace_id = str(uuid.uuid4())
204
212
 
213
+ # If trace retention is disabled, still return a trace_id for correlation
214
+ # without constructing/storing a full trace payload.
215
+ maxlen = getattr(getattr(self, "_traces", None), "maxlen", None)
216
+ if maxlen == 0:
217
+ return trace_id
218
+
205
219
  # Extract generation parameters
206
220
  temperature = kwargs.get('temperature', self.temperature)
207
221
  max_tokens = kwargs.get('max_tokens', self.max_tokens)
@@ -408,6 +422,13 @@ class BaseProvider(AbstractCoreInterface, ABC):
408
422
 
409
423
  # Handle tool execution control
410
424
  should_execute_tools = execute_tools if execute_tools is not None else self.execute_tools
425
+ if should_execute_tools and converted_tools:
426
+ warnings.warn(
427
+ "execute_tools=True is deprecated. Prefer passing tools explicitly to generate() "
428
+ "and executing tool calls in the host/runtime via a ToolExecutor.",
429
+ DeprecationWarning,
430
+ stacklevel=2,
431
+ )
411
432
  if not should_execute_tools and converted_tools:
412
433
  # If tools are provided but execution is disabled,
413
434
  # we still pass them to the provider for generation but won't execute them
@@ -1556,4 +1577,4 @@ Please provide a structured response."""
1556
1577
  # Yield chunks asynchronously
1557
1578
  for chunk in sync_gen:
1558
1579
  yield chunk
1559
- await asyncio.sleep(0) # Yield control to event loop
1580
+ await asyncio.sleep(0) # Yield control to event loop
@@ -1956,6 +1956,39 @@ async def provider_chat_completions(
1956
1956
  _, model = parse_model_string(request.model)
1957
1957
  return await process_chat_completion(provider, model, request, http_request)
1958
1958
 
1959
+
1960
+ def _extract_trace_metadata(http_request: Request) -> Dict[str, Any]:
1961
+ """Extract trace metadata from request headers (schema-safe)."""
1962
+ meta: Dict[str, Any] = {}
1963
+
1964
+ raw = (
1965
+ http_request.headers.get("x-abstractcore-trace-metadata")
1966
+ or http_request.headers.get("x-abstract-trace-metadata")
1967
+ )
1968
+ if raw:
1969
+ try:
1970
+ parsed = json.loads(raw)
1971
+ if isinstance(parsed, dict):
1972
+ meta.update(parsed)
1973
+ except Exception:
1974
+ # Ignore invalid metadata payloads; tracing is best-effort.
1975
+ pass
1976
+
1977
+ header_map = {
1978
+ "actor_id": "x-abstractcore-actor-id",
1979
+ "session_id": "x-abstractcore-session-id",
1980
+ "run_id": "x-abstractcore-run-id",
1981
+ "parent_run_id": "x-abstractcore-parent-run-id",
1982
+ }
1983
+ for key, header in header_map.items():
1984
+ val = http_request.headers.get(header)
1985
+ if val is not None and key not in meta:
1986
+ meta[key] = val
1987
+
1988
+ # Never log or return these directly; they are for internal correlation only.
1989
+ return meta
1990
+
1991
+
1959
1992
  async def process_chat_completion(
1960
1993
  provider: str,
1961
1994
  model: str,
@@ -2019,6 +2052,11 @@ async def process_chat_completion(
2019
2052
  # Create LLM instance
2020
2053
  # Prepare provider-specific kwargs
2021
2054
  provider_kwargs = {}
2055
+ trace_metadata = _extract_trace_metadata(http_request)
2056
+ if trace_metadata:
2057
+ # Enable trace capture (trace_id) without retaining full trace buffers by default.
2058
+ provider_kwargs["enable_tracing"] = True
2059
+ provider_kwargs.setdefault("max_traces", 0)
2022
2060
  if request.base_url:
2023
2061
  provider_kwargs["base_url"] = request.base_url
2024
2062
  logger.info(
@@ -2047,6 +2085,8 @@ async def process_chat_completion(
2047
2085
  "tool_choice": request.tool_choice if request.tools else None,
2048
2086
  "execute_tools": False, # Server mode - don't execute tools
2049
2087
  }
2088
+ if trace_metadata:
2089
+ gen_kwargs["trace_metadata"] = trace_metadata
2050
2090
 
2051
2091
  # Add optional parameters
2052
2092
  if request.stop:
@@ -2081,9 +2121,18 @@ async def process_chat_completion(
2081
2121
  )
2082
2122
  else:
2083
2123
  response = llm.generate(**gen_kwargs)
2084
- return convert_to_openai_response(
2124
+ openai_response = convert_to_openai_response(
2085
2125
  response, provider, model, syntax_rewriter, request_id
2086
2126
  )
2127
+ trace_id = None
2128
+ if hasattr(response, "metadata") and isinstance(getattr(response, "metadata"), dict):
2129
+ trace_id = response.metadata.get("trace_id")
2130
+ if trace_id:
2131
+ return JSONResponse(
2132
+ content=openai_response,
2133
+ headers={"X-AbstractCore-Trace-Id": str(trace_id)},
2134
+ )
2135
+ return openai_response
2087
2136
  finally:
2088
2137
  # Cleanup temporary files (base64 and downloaded images) with delay to avoid race conditions
2089
2138
  import threading
@@ -2408,4 +2457,4 @@ Debug Mode:
2408
2457
  # ============================================================================
2409
2458
 
2410
2459
  if __name__ == "__main__":
2411
- run_server_with_args()
2460
+ run_server_with_args()
@@ -4,42 +4,63 @@ Universal tool support for AbstractCore.
4
4
  This package provides a unified tool system that works across all models
5
5
  and providers, whether they have native tool APIs or require prompting.
6
6
 
7
- Key components:
7
+ Tool Execution Modes
8
+ --------------------
9
+
10
+ AbstractCore supports two tool execution modes:
11
+
12
+ **Passthrough Mode (Default)** - execute_tools=False
13
+ The LLM returns raw tool call tags that downstream runtimes
14
+ (AbstractRuntime, Codex, Claude Code) parse and execute.
15
+ Use case: Agent loops, custom orchestration, multi-step workflows.
16
+
17
+ **Direct Execution Mode** - execute_tools=True
18
+ AbstractCore parses and executes tools internally using the
19
+ global registry. Requires register_tool() for each tool.
20
+ Use case: Simple scripts, single-turn tool use.
21
+
22
+ Key Components
23
+ --------------
8
24
  - Core types (ToolDefinition, ToolCall, ToolResult)
9
25
  - Universal handler for all models
10
26
  - Architecture-based parsing and formatting
11
27
  - Tool registry for managing available tools
12
28
 
13
- Example usage:
29
+ Example: Passthrough Mode (Default)
30
+ -----------------------------------
14
31
  ```python
15
- from abstractcore.tools import ToolDefinition, UniversalToolHandler, register_tool
32
+ from abstractcore import create_llm
33
+ from abstractcore.tools import tool
16
34
 
17
- # Define a tool
18
- def list_files(directory: str = ".", pattern: str = "*") -> str:
19
- '''List files in a directory.'''
20
- import os, fnmatch
21
- files = [f for f in os.listdir(directory) if fnmatch.fnmatch(f, pattern)]
22
- return "\n".join(files)
35
+ @tool(name="get_weather", description="Get weather for a city")
36
+ def get_weather(city: str) -> str:
37
+ return f"Weather in {city}: Sunny"
23
38
 
24
- # Register the tool
25
- tool_def = ToolDefinition.from_function(list_files)
39
+ llm = create_llm("ollama", model="qwen3:4b")
40
+ response = llm.generate("Weather in Paris?", tools=[get_weather])
41
+ # response.content has tool call tags - parse with your runtime
42
+ ```
26
43
 
27
- # Create handler for a model
28
- handler = UniversalToolHandler("qwen3-coder:30b")
44
+ Example: Direct Execution Mode
45
+ ------------------------------
46
+ ```python
47
+ from abstractcore import create_llm
48
+ from abstractcore.tools import tool, register_tool
29
49
 
30
- # Get tool prompt for prompted models
31
- if handler.supports_prompted:
32
- tool_prompt = handler.format_tools_prompt([tool_def])
33
- print("Add this to your system prompt:")
34
- print(tool_prompt)
50
+ @tool(name="get_weather", description="Get weather for a city")
51
+ def get_weather(city: str) -> str:
52
+ return f"Weather in {city}: Sunny"
35
53
 
36
- # Parse response for tool calls
37
- response = "I'll list the files. <|tool_call|>{'name': 'list_files', 'arguments': {'directory': '.'}}"
38
- tool_calls = handler.parse_response(response, mode="prompted")
54
+ register_tool(get_weather) # Required for direct execution
39
55
 
40
- if tool_calls.has_tool_calls():
41
- print("Tool calls found:", tool_calls.tool_calls)
56
+ llm = create_llm("ollama", model="qwen3:4b", execute_tools=True)
57
+ response = llm.generate("Weather in Paris?", tools=[get_weather])
58
+ # response.content has executed tool results
42
59
  ```
60
+
61
+ Note: The @tool decorator creates metadata but does NOT auto-register.
62
+ Tools are passed explicitly to generate(). Use register_tool() only
63
+ when using direct execution mode.
43
64
  """
44
65
 
45
66
  # Core types
@@ -240,7 +240,7 @@ def list_files(directory_path: str = ".", pattern: str = "*", recursive: bool =
240
240
 
241
241
 
242
242
  @tool(
243
- description="Search for text patterns INSIDE files using regex (returns file paths with line numbers by default)",
243
+ description="Search for text patterns INSIDE files and codes using regex (returns file paths with line numbers by default)",
244
244
  tags=["search", "content", "regex", "grep", "text"],
245
245
  when_to_use="When you need to find specific text, code patterns, or content INSIDE files (NOT for finding files by names)",
246
246
  examples=[
@@ -1742,9 +1742,9 @@ def _parse_binary_content(binary_bytes: bytes, content_type: str, include_previe
1742
1742
 
1743
1743
 
1744
1744
  @tool(
1745
- description="Edit files using pattern matching and replacement - simple, powerful, and intuitive",
1746
- tags=["file", "edit", "modify", "pattern", "replace", "regex"],
1747
- when_to_use="When you need to edit files by finding patterns (text, functions, code blocks) and replacing them",
1745
+ description="Edit files by replacing text patterns using simple matching or regex",
1746
+ tags=["file", "edit", "replace", "pattern", "substitute", "regex"],
1747
+ when_to_use="When you need to edit files by replacing text. Supports simple text or regex patterns, line ranges, preview mode, and controlling replacement count.",
1748
1748
  examples=[
1749
1749
  {
1750
1750
  "description": "Replace simple text",
@@ -1764,7 +1764,7 @@ def _parse_binary_content(binary_bytes: bytes, content_type: str, include_previe
1764
1764
  }
1765
1765
  },
1766
1766
  {
1767
- "description": "Replace with occurrence limit",
1767
+ "description": "Replace only first occurrence",
1768
1768
  "arguments": {
1769
1769
  "file_path": "document.txt",
1770
1770
  "pattern": "TODO",
@@ -1780,9 +1780,147 @@ def _parse_binary_content(binary_bytes: bytes, content_type: str, include_previe
1780
1780
  "replacement": "class NewClass",
1781
1781
  "preview_only": True
1782
1782
  }
1783
+ },
1784
+ {
1785
+ "description": "Match pattern ignoring whitespace differences (enabled by default)",
1786
+ "arguments": {
1787
+ "file_path": "script.py",
1788
+ "pattern": "if condition:\n do_something()",
1789
+ "replacement": "if condition:\n do_something_else()",
1790
+ "flexible_whitespace": True
1791
+ }
1783
1792
  }
1784
1793
  ]
1785
1794
  )
1795
+
1796
+
1797
+ def _normalize_escape_sequences(text: str) -> str:
1798
+ """Convert literal escape sequences to actual control characters.
1799
+
1800
+ Handles cases where LLMs send '\\n' (literal) instead of actual newlines.
1801
+ This is a common issue when LLM output is over-escaped in JSON.
1802
+
1803
+ Args:
1804
+ text: Input string potentially containing literal escape sequences
1805
+
1806
+ Returns:
1807
+ String with \\n, \\t, \\r converted to actual control characters
1808
+ """
1809
+ # Only convert if there are literal escape sequences
1810
+ if '\\n' in text or '\\t' in text or '\\r' in text:
1811
+ text = text.replace('\\n', '\n')
1812
+ text = text.replace('\\t', '\t')
1813
+ text = text.replace('\\r', '\r')
1814
+ return text
1815
+
1816
+
1817
+ def _flexible_whitespace_match(
1818
+ pattern: str,
1819
+ replacement: str,
1820
+ content: str,
1821
+ max_replacements: int
1822
+ ) -> Optional[tuple]:
1823
+ """
1824
+ Match pattern with flexible leading whitespace handling.
1825
+
1826
+ Converts a multi-line pattern into a regex that:
1827
+ 1. Normalizes line endings (\r\n -> \n)
1828
+ 2. Matches any amount of leading whitespace on each line
1829
+ 3. Preserves the non-whitespace content exactly
1830
+
1831
+ Returns (updated_content, count) if matches found, None otherwise.
1832
+ """
1833
+ # Normalize line endings in both pattern and content
1834
+ pattern_normalized = pattern.replace('\r\n', '\n')
1835
+ content_normalized = content.replace('\r\n', '\n')
1836
+
1837
+ # Split pattern into lines
1838
+ pattern_lines = pattern_normalized.split('\n')
1839
+
1840
+ # Build regex parts for each line
1841
+ regex_parts = []
1842
+ for i, line in enumerate(pattern_lines):
1843
+ # Get leading whitespace and content
1844
+ stripped = line.lstrip()
1845
+ if stripped:
1846
+ # Escape special regex characters in the content
1847
+ escaped_content = re.escape(stripped)
1848
+ # Match any leading whitespace (spaces or tabs)
1849
+ regex_parts.append(r'[ \t]*' + escaped_content)
1850
+ else:
1851
+ # Empty line or whitespace-only - match any whitespace
1852
+ regex_parts.append(r'[ \t]*')
1853
+
1854
+ # Join with flexible newline matching (handles \n or \r\n)
1855
+ flexible_pattern = r'\r?\n'.join(regex_parts)
1856
+
1857
+ try:
1858
+ regex = re.compile(flexible_pattern, re.MULTILINE)
1859
+ except re.error:
1860
+ return None
1861
+
1862
+ matches = list(regex.finditer(content_normalized))
1863
+ if not matches:
1864
+ return None
1865
+
1866
+ # Apply replacements
1867
+ # For the replacement, we need to adjust indentation to match
1868
+ # the actual indentation found in the match
1869
+
1870
+ def replacement_fn(match):
1871
+ """Adjust replacement to use the indentation from the matched text."""
1872
+ matched_text = match.group(0)
1873
+ matched_lines = matched_text.split('\n')
1874
+
1875
+ # Normalize the replacement's line endings
1876
+ repl_normalized = replacement.replace('\r\n', '\n')
1877
+ repl_lines = repl_normalized.split('\n')
1878
+
1879
+ if not repl_lines:
1880
+ return replacement
1881
+
1882
+ # For each line in the replacement, use the corresponding matched line's
1883
+ # actual indentation. This preserves the file's indentation style exactly.
1884
+ adjusted_lines = []
1885
+ for j, repl_line in enumerate(repl_lines):
1886
+ repl_stripped = repl_line.lstrip()
1887
+
1888
+ if j < len(matched_lines):
1889
+ # We have a corresponding matched line - use its actual indentation
1890
+ matched_line = matched_lines[j]
1891
+ actual_indent_str = matched_line[:len(matched_line) - len(matched_line.lstrip())]
1892
+ adjusted_lines.append(actual_indent_str + repl_stripped)
1893
+ else:
1894
+ # Extra lines in replacement - no matched counterpart
1895
+ # Use the indentation from the last matched line as reference
1896
+ if matched_lines:
1897
+ last_matched = matched_lines[-1]
1898
+ base_indent_str = last_matched[:len(last_matched) - len(last_matched.lstrip())]
1899
+ # Add relative indentation from replacement
1900
+ repl_indent_len = len(repl_line) - len(repl_stripped)
1901
+ pattern_last_indent = len(pattern_lines[-1]) - len(pattern_lines[-1].lstrip()) if pattern_lines else 0
1902
+ extra_spaces = max(0, repl_indent_len - pattern_last_indent)
1903
+ adjusted_lines.append(base_indent_str + ' ' * extra_spaces + repl_stripped)
1904
+ else:
1905
+ adjusted_lines.append(repl_line)
1906
+
1907
+ return '\n'.join(adjusted_lines)
1908
+
1909
+ # Apply the replacement
1910
+ if max_replacements == -1:
1911
+ updated = regex.sub(replacement_fn, content_normalized)
1912
+ count = len(matches)
1913
+ else:
1914
+ updated = regex.sub(replacement_fn, content_normalized, count=max_replacements)
1915
+ count = min(len(matches), max_replacements)
1916
+
1917
+ # Restore original line endings if needed
1918
+ if '\r\n' in content and '\r\n' not in updated:
1919
+ updated = updated.replace('\n', '\r\n')
1920
+
1921
+ return (updated, count)
1922
+
1923
+
1786
1924
  def edit_file(
1787
1925
  file_path: str,
1788
1926
  pattern: str,
@@ -1792,12 +1930,14 @@ def edit_file(
1792
1930
  start_line: Optional[int] = None,
1793
1931
  end_line: Optional[int] = None,
1794
1932
  preview_only: bool = False,
1795
- encoding: str = "utf-8"
1933
+ encoding: str = "utf-8",
1934
+ flexible_whitespace: bool = True
1796
1935
  ) -> str:
1797
1936
  """
1798
- Edit files using pattern matching and replacement.
1937
+ Replace text patterns in files using pattern matching.
1799
1938
 
1800
1939
  Finds patterns (text or regex) in files and replaces them with new content.
1940
+ For complex multi-line edits, consider using edit_file with unified diff instead.
1801
1941
 
1802
1942
  Args:
1803
1943
  file_path: Path to the file to edit
@@ -1809,6 +1949,10 @@ def edit_file(
1809
1949
  end_line: Ending line number to limit search scope (1-indexed, optional)
1810
1950
  preview_only: Show what would be changed without applying (default: False)
1811
1951
  encoding: File encoding (default: "utf-8")
1952
+ flexible_whitespace: Enable whitespace-flexible matching (default: True).
1953
+ When enabled, matches patterns even if indentation differs between
1954
+ the pattern and file content. Handles tabs vs spaces, different
1955
+ indentation levels, and line ending differences (\n vs \r\n).
1812
1956
 
1813
1957
  Returns:
1814
1958
  Success message with replacement details or error message
@@ -1839,6 +1983,10 @@ def edit_file(
1839
1983
 
1840
1984
  original_content = content
1841
1985
 
1986
+ # Normalize escape sequences - handles LLMs sending \\n instead of actual newlines
1987
+ pattern = _normalize_escape_sequences(pattern)
1988
+ replacement = _normalize_escape_sequences(replacement)
1989
+
1842
1990
  # Handle line range targeting if specified
1843
1991
  search_content = content
1844
1992
  line_offset = 0
@@ -1889,16 +2037,31 @@ def edit_file(
1889
2037
  else:
1890
2038
  # Simple text replacement on search content
1891
2039
  count = search_content.count(pattern)
1892
- if count == 0:
2040
+
2041
+ # If exact match fails and flexible_whitespace is enabled, try flexible matching
2042
+ if count == 0 and flexible_whitespace and '\n' in pattern:
2043
+ # Convert pattern to regex with flexible leading whitespace per line
2044
+ # Strategy: Replace each newline + whitespace with a regex that matches
2045
+ # any amount of leading whitespace
2046
+ flexible_result = _flexible_whitespace_match(
2047
+ pattern, replacement, search_content, max_replacements
2048
+ )
2049
+ if flexible_result is not None:
2050
+ updated_search_content, replacements_made = flexible_result
2051
+ else:
2052
+ range_info = f" (lines {start_line}-{end_line})" if start_line is not None or end_line is not None else ""
2053
+ return f"No occurrences of '{pattern}' found in '{file_path}'{range_info}"
2054
+ elif count == 0:
1893
2055
  range_info = f" (lines {start_line}-{end_line})" if start_line is not None or end_line is not None else ""
1894
2056
  return f"No occurrences of '{pattern}' found in '{file_path}'{range_info}"
1895
-
1896
- if max_replacements == -1:
1897
- updated_search_content = search_content.replace(pattern, replacement)
1898
- replacements_made = count
1899
2057
  else:
1900
- updated_search_content = search_content.replace(pattern, replacement, max_replacements)
1901
- replacements_made = min(count, max_replacements)
2058
+ # Exact match found
2059
+ if max_replacements == -1:
2060
+ updated_search_content = search_content.replace(pattern, replacement)
2061
+ replacements_made = count
2062
+ else:
2063
+ updated_search_content = search_content.replace(pattern, replacement, max_replacements)
2064
+ replacements_made = min(count, max_replacements)
1902
2065
 
1903
2066
  # Reconstruct the full file content if line ranges were used
1904
2067
  if start_line is not None or end_line is not None:
@@ -1988,7 +2151,6 @@ def edit_file(
1988
2151
  return f"❌ Error editing file: {str(e)}"
1989
2152
 
1990
2153
 
1991
-
1992
2154
  @tool(
1993
2155
  description="Execute shell commands safely with security controls and platform detection",
1994
2156
  tags=["command", "shell", "execution", "system"],
@@ -148,10 +148,64 @@ def format_tool_prompt(tools: List[ToolDefinition], model_name: Optional[str] =
148
148
 
149
149
  # Internal helpers
150
150
 
151
+ def _sanitize_tool_call_tags(response: str) -> str:
152
+ """
153
+ Sanitize malformed tool call tags before parsing.
154
+
155
+ Handles common LLM output malformations:
156
+ - Doubled opening tags: <|tool_call|><|tool_call|> → <|tool_call|>
157
+ - Doubled closing tags: </|tool_call|></|tool_call|> → </|tool_call|>
158
+ - Malformed closing with }: </|tool_call|} → </|tool_call|>
159
+
160
+ Args:
161
+ response: Raw model response text
162
+
163
+ Returns:
164
+ Sanitized response with normalized tool call syntax
165
+ """
166
+ if not response:
167
+ return response
168
+
169
+ original = response
170
+
171
+ # Fix doubled/multiple opening tags (collapse to single)
172
+ # Handles: <|tool_call|><|tool_call|> or <|tool_call|>\n<|tool_call|>
173
+ response = re.sub(
174
+ r'(<\|tool_call\|>\s*)+',
175
+ r'<|tool_call|>',
176
+ response,
177
+ flags=re.IGNORECASE
178
+ )
179
+
180
+ # Fix malformed closing tags with } instead of |>
181
+ # Handles: </|tool_call|} → </|tool_call|>
182
+ response = re.sub(
183
+ r'</\|tool_call\|\}',
184
+ r'</|tool_call|>',
185
+ response,
186
+ flags=re.IGNORECASE
187
+ )
188
+
189
+ # Fix doubled/multiple closing tags (collapse to single)
190
+ response = re.sub(
191
+ r'(</\|tool_call\|>\s*)+',
192
+ r'</|tool_call|>',
193
+ response,
194
+ flags=re.IGNORECASE
195
+ )
196
+
197
+ if response != original:
198
+ logger.debug(f"Sanitized malformed tool call tags")
199
+
200
+ return response
201
+
202
+
151
203
  def _get_tool_format(model_name: Optional[str]) -> ToolFormat:
152
204
  """Get tool format for a model."""
153
205
  if not model_name:
154
- return ToolFormat.RAW_JSON
206
+ # When no model specified, use NATIVE which triggers _parse_any_format
207
+ # This ensures all formats are tried including <|tool_call|> special tokens
208
+ return ToolFormat.NATIVE
155
209
 
156
210
  architecture = detect_architecture(model_name)
157
211
  arch_format = get_architecture_format(architecture)
@@ -175,7 +229,10 @@ def _get_tool_format(model_name: Optional[str]) -> ToolFormat:
175
229
  def _parse_special_token(response: str) -> List[ToolCall]:
176
230
  """Parse Qwen-style <|tool_call|> format with robust fallback."""
177
231
  tool_calls = []
178
-
232
+
233
+ # SANITIZE FIRST: Fix malformed tags (doubled tags, broken closing tags)
234
+ response = _sanitize_tool_call_tags(response)
235
+
179
236
  # Pre-process: Remove markdown code fences that might wrap tool calls
180
237
  # This handles cases like ```json\n<|tool_call|>...\n```
181
238
  cleaned_response = re.sub(r'```(?:json|python|tool_code|tool_call)?\s*\n', '', response, flags=re.IGNORECASE)
@@ -250,8 +307,40 @@ def _parse_special_token(response: str) -> List[ToolCall]:
250
307
  try:
251
308
  tool_data = json.loads(json_str)
252
309
  except json.JSONDecodeError:
253
- # Fallback: fix common LLM JSON issues (unescaped newlines)
254
- fixed_json = json_str.replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
310
+ # Fallback: Escape newlines/tabs only inside JSON string values
311
+ # This prevents escaping structural newlines which would break parsing
312
+ # Algorithm: Track when inside/outside strings, only escape within strings
313
+ in_string = False
314
+ escaped = False
315
+ fixed = []
316
+
317
+ for char in json_str:
318
+ if escaped:
319
+ # Previous char was backslash, this is part of escape sequence
320
+ fixed.append(char)
321
+ escaped = False
322
+ elif char == '\\':
323
+ # Start of escape sequence
324
+ fixed.append(char)
325
+ escaped = True
326
+ elif char == '"':
327
+ # Toggle string context
328
+ in_string = not in_string
329
+ fixed.append(char)
330
+ elif in_string and char == '\n':
331
+ # Newline inside string - escape it
332
+ fixed.append('\\n')
333
+ elif in_string and char == '\r':
334
+ # CR inside string - escape it
335
+ fixed.append('\\r')
336
+ elif in_string and char == '\t':
337
+ # Tab inside string - escape it
338
+ fixed.append('\\t')
339
+ else:
340
+ # Normal character or structural whitespace
341
+ fixed.append(char)
342
+
343
+ fixed_json = ''.join(fixed)
255
344
  tool_data = json.loads(fixed_json)
256
345
 
257
346
  if isinstance(tool_data, dict):
@@ -424,6 +513,9 @@ def _parse_raw_json(response: str) -> List[ToolCall]:
424
513
 
425
514
  def _parse_any_format(response: str) -> List[ToolCall]:
426
515
  """Try all parsing formats with comprehensive fallbacks."""
516
+ # SANITIZE FIRST: Fix malformed tags before trying any parser
517
+ response = _sanitize_tool_call_tags(response)
518
+
427
519
  tool_calls = []
428
520
 
429
521
  # Try each parser and accumulate results
@@ -6,6 +6,7 @@ and executing them safely.
6
6
  """
7
7
 
8
8
  import time
9
+ import warnings
9
10
  from typing import Dict, List, Any, Callable, Optional, Union
10
11
  from functools import wraps
11
12
 
@@ -270,6 +271,12 @@ def register_tool(tool: Union[ToolDefinition, Callable]) -> ToolDefinition:
270
271
  Returns:
271
272
  The registered ToolDefinition
272
273
  """
274
+ warnings.warn(
275
+ "Global tool registration is deprecated. Prefer passing tools explicitly to generate() "
276
+ "and executing tool calls via a host-configured ToolExecutor.",
277
+ DeprecationWarning,
278
+ stacklevel=2,
279
+ )
273
280
  return _global_registry.register(tool)
274
281
 
275
282
 
@@ -308,19 +315,11 @@ def clear_registry():
308
315
  return _global_registry.clear()
309
316
 
310
317
 
311
- def tool(func: Callable) -> Callable:
312
- """
313
- Decorator to register a function as a tool.
314
-
315
- Args:
316
- func: Function to register as a tool
317
-
318
- Returns:
319
- The original function (unchanged)
320
- """
321
- register_tool(func)
322
- return func
323
-
324
-
325
- # Convenience decorator alias
326
- register = tool
318
+ __all__ = [
319
+ "ToolRegistry",
320
+ "get_registry",
321
+ "register_tool",
322
+ "execute_tool",
323
+ "execute_tools",
324
+ "clear_registry",
325
+ ]
@@ -11,4 +11,4 @@ including when the package is installed from PyPI where pyproject.toml is not av
11
11
 
12
12
  # Package version - update this when releasing new versions
13
13
  # This must be manually synchronized with the version in pyproject.toml
14
- __version__ = "2.6.6"
14
+ __version__ = "2.6.8"
@@ -24,12 +24,7 @@ from typing import Tuple, Dict, Any, Optional, List
24
24
  from pathlib import Path
25
25
  import logging
26
26
 
27
- try:
28
- from PIL import Image
29
- except ImportError as e:
30
- raise ImportError(
31
- "PIL (Pillow) is required for VLM token calculation. Install with: pip install Pillow"
32
- ) from e
27
+ from PIL import Image
33
28
 
34
29
  from ..utils.structured_logging import get_logger
35
30
  from ..architectures.detection import get_model_capabilities, detect_architecture
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractcore
3
- Version: 2.6.6
3
+ Version: 2.6.8
4
4
  Summary: Unified interface to all LLM providers with essential infrastructure for tool calling, streaming, and model management
5
5
  Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
6
6
  Maintainer-email: Laurent-Philippe Albou <contact@abstractcore.ai>
@@ -30,6 +30,7 @@ Requires-Dist: pydantic<3.0.0,>=2.0.0
30
30
  Requires-Dist: httpx<1.0.0,>=0.24.0
31
31
  Requires-Dist: tiktoken<1.0.0,>=0.5.0
32
32
  Requires-Dist: requests<3.0.0,>=2.25.0
33
+ Requires-Dist: Pillow<12.0.0,>=10.0.0
33
34
  Provides-Extra: openai
34
35
  Requires-Dist: openai<2.0.0,>=1.0.0; extra == "openai"
35
36
  Provides-Extra: anthropic
@@ -194,6 +195,50 @@ response = llm.generate(
194
195
  print(response.content)
195
196
  ```
196
197
 
198
+ ### Tool Execution Modes
199
+
200
+ AbstractCore supports two tool execution modes:
201
+
202
+ **Mode 1: Passthrough (Default)** - Returns raw tool call tags for downstream processing
203
+
204
+ ```python
205
+ from abstractcore import create_llm
206
+ from abstractcore.tools import tool
207
+
208
+ @tool(name="get_weather", description="Get weather for a city")
209
+ def get_weather(city: str) -> str:
210
+ return f"Weather in {city}: Sunny, 22°C"
211
+
212
+ llm = create_llm("ollama", model="qwen3:4b") # execute_tools=False by default
213
+ response = llm.generate("What's the weather in Paris?", tools=[get_weather])
214
+ # response.content contains raw tool call tags: <|tool_call|>...
215
+ # Downstream runtime (AbstractRuntime, Codex, Claude Code) parses and executes
216
+ ```
217
+
218
+ **Use case**: Agent loops, AbstractRuntime, Codex, Claude Code, custom orchestration
219
+
220
+ **Mode 2: Direct Execution** - AbstractCore executes tools and returns results
221
+
222
+ ```python
223
+ from abstractcore import create_llm
224
+ from abstractcore.tools import tool
225
+ from abstractcore.tools.registry import register_tool
226
+
227
+ @tool(name="get_weather", description="Get weather for a city")
228
+ def get_weather(city: str) -> str:
229
+ return f"Weather in {city}: Sunny, 22°C"
230
+
231
+ register_tool(get_weather) # Required for direct execution
232
+
233
+ llm = create_llm("ollama", model="qwen3:4b", execute_tools=True)
234
+ response = llm.generate("What's the weather in Paris?", tools=[get_weather])
235
+ # response.content contains executed tool results
236
+ ```
237
+
238
+ **Use case**: Simple scripts, single-turn tool use
239
+
240
+ > **Note**: The `@tool` decorator creates metadata but does NOT register globally. Tools are passed explicitly to `generate()`. Use `register_tool()` only when using direct execution mode.
241
+
197
242
  ### Response Object (GenerateResponse)
198
243
 
199
244
  Every LLM generation returns a **GenerateResponse** object with consistent structure across all providers:
@@ -12,7 +12,7 @@ abstractcore/architectures/__init__.py,sha256=-4JucAM7JkMWShWKkePoclxrUHRKgaG36U
12
12
  abstractcore/architectures/detection.py,sha256=jmpD04xcKotWCW7--jadBzCtD2a5dYJi1zljpxB9JmU,19813
13
13
  abstractcore/architectures/enums.py,sha256=9vIv2vDBEKhxwzwH9iaSAyf-iVj3p8y9loMeN_mYTJ8,3821
14
14
  abstractcore/assets/architecture_formats.json,sha256=Yf-W1UuadrL8qid0pfU_pEBOkjwH4lvx7Nzu83GACp8,16622
15
- abstractcore/assets/model_capabilities.json,sha256=tmfQNNPTNJAauF51nPYkMqcjc17F2geeYngh0HjEUZ0,68836
15
+ abstractcore/assets/model_capabilities.json,sha256=d3rC-nqXJ5LDeG0MWEcODcyEqzF4A8FWe27fNcCBBPY,69235
16
16
  abstractcore/assets/session_schema.json,sha256=hMCVrq3KSyVExrMGzuykf7bU-z6WyIVuEGU8du9_zUY,10570
17
17
  abstractcore/compression/__init__.py,sha256=svwaHCHoLkKp1IJKdlSC72k6b8vOUOqVF-Yek8ZHwj8,915
18
18
  abstractcore/compression/analytics.py,sha256=3orxJkjjMQE_6mID8_8C0sRQ4Gr2Pw7kQ-Iy0p7pIgM,16042
@@ -60,16 +60,16 @@ abstractcore/media/processors/office_processor.py,sha256=_aTrrDtREiy6MivbANFc1FK
60
60
  abstractcore/media/processors/pdf_processor.py,sha256=qniYt7cTYYPVRi_cS1IsXztOldeY0bqdn7sdbELBU9k,17157
61
61
  abstractcore/media/processors/text_processor.py,sha256=D84QWxxIou4MeNhERmCTxi_p27CgicVFhMXJiujZgIE,21905
62
62
  abstractcore/media/utils/__init__.py,sha256=30-CTif91iRKOXJ4njGiduWAt-xp31U7NafMBNvgdO0,460
63
- abstractcore/media/utils/image_scaler.py,sha256=RWE2kPeURLEtJDK-Qs4KvZKtu-GkMLziFL9Vc9-aWjc,11388
64
- abstractcore/processing/__init__.py,sha256=QcACEnhnHKYCkFL1LNOW_uqBrwkTAmz5A61N4K2dyu0,988
63
+ abstractcore/media/utils/image_scaler.py,sha256=uUVF91_mLTt-PR0LZiDawTolE2qIrTG1rzV9oYVbxhg,11171
64
+ abstractcore/processing/__init__.py,sha256=T9DPd6iLo06opJ2DrUIjmqGhYcvvYNTfuOp0DNEoU5Q,1024
65
65
  abstractcore/processing/basic_deepsearch.py,sha256=dzJQtH4k44XY9tvG0Z4JIlYt_s7HpbLdSPScha-t7vk,101036
66
66
  abstractcore/processing/basic_extractor.py,sha256=3x-3BdIHgLvqLnLF6K1-P4qVaLIpAnNIIutaJi7lDQM,49832
67
67
  abstractcore/processing/basic_intent.py,sha256=wD99Z7fE2RiYk6oyTZXojUbv-bz8HhKFIuIHYLLTw54,32455
68
68
  abstractcore/processing/basic_judge.py,sha256=L1fc9H0-_88B1TULL-mlaNL7OydMgp-ru_zzzoGdr38,37220
69
- abstractcore/processing/basic_summarizer.py,sha256=XHNxMQ_8aLStTeUo6_2JaThlct12Htpz7ORmm0iuJsg,25495
69
+ abstractcore/processing/basic_summarizer.py,sha256=XYAKpsOiqVGt1Rl9K6Uaa3YyANc-zSvqD4qH0KAnffM,28603
70
70
  abstractcore/providers/__init__.py,sha256=dNz-KrUwpBZhEv6DkAe3t_V8w40_HjeME5j9VL0lDyo,1886
71
71
  abstractcore/providers/anthropic_provider.py,sha256=0-qZb0Es6-VLuVVl2j7IUjOuyRlgjQdJFulWfpi4qb4,31740
72
- abstractcore/providers/base.py,sha256=nWF1pxeUlT4ozlUqKG0rWOmLkfo-zQgfU7fv3AUSI08,68452
72
+ abstractcore/providers/base.py,sha256=yP75mfBW_SM7IEcMO_Sl-zeNq-IjzJWzYq-ouMOyRQs,69402
73
73
  abstractcore/providers/huggingface_provider.py,sha256=v4UUmODrnWKtTygzPh-lm4jSCAPms5VYJE5v7PWB4Lo,79458
74
74
  abstractcore/providers/lmstudio_provider.py,sha256=92_vx7AVVt_oufJdHo3R0D_V2qyTKO2DKzi9-l4KzWs,34114
75
75
  abstractcore/providers/mlx_provider.py,sha256=afLCEwuw7r8OK4fD3OriyKMcWpxVIob_37ItmgAclfc,23123
@@ -81,16 +81,16 @@ abstractcore/providers/registry.py,sha256=gz2fu3m7EVYHWy9Lggbmjh46abrdnxC3_73ccI
81
81
  abstractcore/providers/streaming.py,sha256=HaGkoItPWXqgml3C-KiPc0hBNpztLzjl_ooECw11BHI,31370
82
82
  abstractcore/providers/vllm_provider.py,sha256=zl1utRG-G__Qh5UpgIEU-Dbb6w5LeZfiboBUC5aPeL0,33969
83
83
  abstractcore/server/__init__.py,sha256=1DSAz_YhQtnKv7sNi5TMQV8GFujctDOabgvAdilQE0o,249
84
- abstractcore/server/app.py,sha256=AYupb0rlmf4-L9xDb7qHVLdUi8lGC-jYgg1Pe_AvZ3o,97507
84
+ abstractcore/server/app.py,sha256=XQueggO5TtkhhxLaVQQ7pxpIPbh4QqB6NcwQ9GJwzAQ,99418
85
85
  abstractcore/structured/__init__.py,sha256=VXRQHGcm-iaYnLOBPin2kyhvhhQA0kaGt_pcNDGsE_8,339
86
86
  abstractcore/structured/handler.py,sha256=hcUe_fZcwx0O3msLqFiOsj6-jbq3S-ZQa9c1nRIZvuo,24622
87
87
  abstractcore/structured/retry.py,sha256=BN_PvrWybyU1clMy2cult1-TVxFSMaVqiCPmmXvA5aI,3805
88
- abstractcore/tools/__init__.py,sha256=oh6vG0RdM1lqUtOp95mLrTsWLh9VmhJf5_FVjGIP5_M,2259
89
- abstractcore/tools/common_tools.py,sha256=hn4Y-HmpYAH3nLnKqJayWr-mpou3T5Ixu-LRdeO1_c4,95161
88
+ abstractcore/tools/__init__.py,sha256=JPPLRLMnwJorbudHL3sWZrJVKA_IHuZLnOGrpHYliGY,3034
89
+ abstractcore/tools/common_tools.py,sha256=zSE5H8PkOQ3SHYi9-srmfnTlkl9jWoKc0TEq9G53ibc,102148
90
90
  abstractcore/tools/core.py,sha256=lUUGihyceiRYlKUFvEMig9jWFF563d574mSDbYYD3fM,4777
91
91
  abstractcore/tools/handler.py,sha256=nBbbBNq029s8pV6NbTU-VmQ6xoSTdt7Af1-XbcMdgU4,12050
92
- abstractcore/tools/parser.py,sha256=AYM0-baSGGGdwd_w2My3B3sOiw2flE2x59mLmdpwJ0A,27768
93
- abstractcore/tools/registry.py,sha256=eI0qKrauT1PzLIfaporXCjBMojmWYzgfd96xdN_Db1g,9087
92
+ abstractcore/tools/parser.py,sha256=Otn1rL2JVTN87CYQuLdXlSM1wTW_Juf2PMsDCJpYzuw,30979
93
+ abstractcore/tools/registry.py,sha256=2C8zNJqVQwBnmnkWogvnNwCktJIBvdo4lA9W5HxJtm8,9184
94
94
  abstractcore/tools/syntax_rewriter.py,sha256=_lBvIJ_gFleR5JZI0UuukQ47pe5CXp3IM9Oee0ypx60,17346
95
95
  abstractcore/tools/tag_rewriter.py,sha256=2hjLoIjVs4277mPBSrNsnxLOVvmXm83xtF0WCOU3uno,20350
96
96
  abstractcore/utils/__init__.py,sha256=8uvIU1WpUfetvWA90PPvXtxnFNjMQF0umb8_FuK2cTA,779
@@ -100,11 +100,11 @@ abstractcore/utils/self_fixes.py,sha256=1VYxPq-q7_DtNl39NbrzUmyHpkhb9Q2SdnXUj4c0
100
100
  abstractcore/utils/structured_logging.py,sha256=Vm-HviSa42G9DJCWmaEv4a0QG3NMsADD3ictLOs4En0,19952
101
101
  abstractcore/utils/token_utils.py,sha256=eLwFmJ68p9WMFD_MHLMmeJRW6Oqx_4hKELB8FNQ2Mnk,21097
102
102
  abstractcore/utils/trace_export.py,sha256=MD1DHDWltpewy62cYzz_OSPAA6edZbZq7_pZbvxz_H8,9279
103
- abstractcore/utils/version.py,sha256=Z8uyHNTgXcYjIz0T3S0B0d_LcOXgKpyd_2WS8Rg3MtU,605
104
- abstractcore/utils/vlm_token_calculator.py,sha256=pIDc_iwJ4IrM_e8AxY2HDU8UdTYitBOXVhu0F-EaMTY,28144
105
- abstractcore-2.6.6.dist-info/licenses/LICENSE,sha256=PI2v_4HMvd6050uDD_4AY_8PzBnu2asa3RKbdDjowTA,1078
106
- abstractcore-2.6.6.dist-info/METADATA,sha256=Z4_SfHiSDLDXMDGLWdsd7cZASoZg2DtXgCQfnjrB6HA,45799
107
- abstractcore-2.6.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
- abstractcore-2.6.6.dist-info/entry_points.txt,sha256=jXNdzeltVs23A2JM2e2HOiAHldHrsnud3EvPI5VffOs,658
109
- abstractcore-2.6.6.dist-info/top_level.txt,sha256=DiNHBI35SIawW3N9Z-z0y6cQYNbXd32pvBkW0RLfScs,13
110
- abstractcore-2.6.6.dist-info/RECORD,,
103
+ abstractcore/utils/version.py,sha256=BjFWFsKdU0RCZy3o-rumfIgdOnuCfs0EPAMpSfaof5Y,605
104
+ abstractcore/utils/vlm_token_calculator.py,sha256=KMhV97gYpiWHYNnPR5yFLw6eA1CPKQ1c-ihPdns72Wg,27979
105
+ abstractcore-2.6.8.dist-info/licenses/LICENSE,sha256=PI2v_4HMvd6050uDD_4AY_8PzBnu2asa3RKbdDjowTA,1078
106
+ abstractcore-2.6.8.dist-info/METADATA,sha256=oSh7ayXP7hwFd83EqAWpmBnkB9PI8of1TWmaudzVYY0,47485
107
+ abstractcore-2.6.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
+ abstractcore-2.6.8.dist-info/entry_points.txt,sha256=jXNdzeltVs23A2JM2e2HOiAHldHrsnud3EvPI5VffOs,658
109
+ abstractcore-2.6.8.dist-info/top_level.txt,sha256=DiNHBI35SIawW3N9Z-z0y6cQYNbXd32pvBkW0RLfScs,13
110
+ abstractcore-2.6.8.dist-info/RECORD,,