abstractcore 2.6.8__py3-none-any.whl → 2.6.9__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,26 +638,17 @@
638
638
  "max_tokens": 262144
639
639
  },
640
640
  "qwen3-coder-30b": {
641
- "max_output_tokens": 65536,
641
+ "max_output_tokens": 8192,
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
- "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",
647
+ "notes": "Code-focused model with native tool support via chatml-function-calling format",
648
+ "source": "Alibaba official docs",
654
649
  "canonical_name": "qwen3-coder-30b",
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
650
+ "aliases": [],
651
+ "max_tokens": 32768
661
652
  },
662
653
  "qwen2-vl": {
663
654
  "max_output_tokens": 8192,
@@ -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, CompressionMode
8
+ from .basic_summarizer import BasicSummarizer, SummaryStyle, SummaryLength
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', 'CompressionMode',
15
+ 'BasicSummarizer', 'SummaryStyle', 'SummaryLength',
16
16
  'BasicExtractor',
17
17
  'BasicJudge', 'JudgmentCriteria', 'Assessment', 'create_judge',
18
18
  'BasicDeepSearch', 'ResearchReport', 'ResearchFinding', 'ResearchPlan', 'ResearchSubTask',
@@ -35,42 +35,6 @@ 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
-
74
38
  class LLMSummaryOutput(BaseModel):
75
39
  """LLM-generated summary output (without word counts)"""
76
40
  summary: str = Field(description="The main summary text")
@@ -529,8 +493,7 @@ Create a unified summary that represents the entire document effectively."""
529
493
  self,
530
494
  messages: List[dict],
531
495
  preserve_recent: int = 6,
532
- focus: Optional[str] = None,
533
- compression_mode: CompressionMode = CompressionMode.STANDARD
496
+ focus: Optional[str] = None
534
497
  ) -> SummaryOutput:
535
498
  """
536
499
  Specialized method for chat history summarization following SOTA 2025 practices
@@ -539,7 +502,6 @@ Create a unified summary that represents the entire document effectively."""
539
502
  messages: List of message dicts with 'role' and 'content' keys
540
503
  preserve_recent: Number of recent messages to keep intact (default 6)
541
504
  focus: Optional focus for summarization (e.g., "key decisions", "technical solutions")
542
- compression_mode: How aggressively to compress (LIGHT, STANDARD, HEAVY)
543
505
 
544
506
  Returns:
545
507
  SummaryOutput: Structured summary optimized for chat history context
@@ -549,67 +511,36 @@ Create a unified summary that represents the entire document effectively."""
549
511
  - Focuses on decisions, solutions, and ongoing topics
550
512
  - Maintains user intent and assistant responses
551
513
  - 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
557
514
  """
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
-
584
515
  if len(messages) <= preserve_recent:
585
516
  # If short enough, just summarize normally
586
- logger.debug("Chat history is short, using standard summarization",
587
- message_count=len(messages),
517
+ logger.debug("Chat history is short, using standard summarization",
518
+ message_count=len(messages),
588
519
  preserve_recent=preserve_recent)
589
520
  chat_text = self._format_chat_messages_to_text(messages)
590
521
  return self.summarize(
591
522
  chat_text,
592
- focus=effective_focus,
523
+ focus=focus or "conversational context and key information",
593
524
  style=SummaryStyle.CONVERSATIONAL,
594
- length=target_length
525
+ length=SummaryLength.STANDARD
595
526
  )
596
527
 
597
528
  # Split into older messages (to summarize) and recent messages (to preserve)
598
529
  older_messages = messages[:-preserve_recent]
599
530
  recent_messages = messages[-preserve_recent:]
600
-
601
- logger.debug("Splitting chat history for summarization",
531
+
532
+ logger.debug("Splitting chat history for summarization",
602
533
  total_messages=len(messages),
603
534
  older_messages=len(older_messages),
604
535
  recent_messages=len(recent_messages))
605
536
 
606
- # Summarize older messages with conversational focus and compression mode
537
+ # Summarize older messages with conversational focus
607
538
  older_text = self._format_chat_messages_to_text(older_messages)
608
539
  older_summary = self.summarize(
609
540
  older_text,
610
- focus=effective_focus,
541
+ focus=focus or "key decisions, solutions, and ongoing context",
611
542
  style=SummaryStyle.CONVERSATIONAL,
612
- length=target_length
543
+ length=SummaryLength.DETAILED
613
544
  )
614
545
 
615
546
  # The summary should ONLY contain the older messages summary
@@ -5,7 +5,6 @@ Base provider with integrated telemetry, events, and exception handling.
5
5
  import time
6
6
  import uuid
7
7
  import asyncio
8
- import warnings
9
8
  from collections import deque
10
9
  from typing import List, Dict, Any, Optional, Union, Iterator, AsyncIterator, Type
11
10
  from abc import ABC, abstractmethod
@@ -61,13 +60,6 @@ class BaseProvider(AbstractCoreInterface, ABC):
61
60
  # execute_tools: True = AbstractCore executes tools (legacy mode)
62
61
  # False = Pass-through mode (default - for API server / agentic CLI)
63
62
  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
- )
71
63
 
72
64
  # Setup retry manager with optional configuration
73
65
  retry_config = kwargs.get('retry_config', None)
@@ -210,12 +202,6 @@ class BaseProvider(AbstractCoreInterface, ABC):
210
202
  """
211
203
  trace_id = str(uuid.uuid4())
212
204
 
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
-
219
205
  # Extract generation parameters
220
206
  temperature = kwargs.get('temperature', self.temperature)
221
207
  max_tokens = kwargs.get('max_tokens', self.max_tokens)
@@ -422,13 +408,6 @@ class BaseProvider(AbstractCoreInterface, ABC):
422
408
 
423
409
  # Handle tool execution control
424
410
  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
- )
432
411
  if not should_execute_tools and converted_tools:
433
412
  # If tools are provided but execution is disabled,
434
413
  # we still pass them to the provider for generation but won't execute them
@@ -1511,10 +1490,28 @@ Please provide a structured response."""
1511
1490
  Returns:
1512
1491
  GenerateResponse, AsyncIterator[GenerateResponse] for streaming, or BaseModel for structured output
1513
1492
  """
1514
- return await self._agenerate_internal(
1493
+ response = await self._agenerate_internal(
1515
1494
  prompt, messages, system_prompt, tools, media, stream, **kwargs
1516
1495
  )
1517
1496
 
1497
+ # Capture interaction trace if enabled (match sync generate_with_telemetry behavior)
1498
+ # Only for non-streaming responses that are GenerateResponse objects
1499
+ if not stream and self.enable_tracing and response and isinstance(response, GenerateResponse):
1500
+ trace_id = self._capture_trace(
1501
+ prompt=prompt,
1502
+ messages=messages,
1503
+ system_prompt=system_prompt,
1504
+ tools=tools,
1505
+ response=response,
1506
+ kwargs=kwargs
1507
+ )
1508
+ # Attach trace_id to response metadata
1509
+ if not response.metadata:
1510
+ response.metadata = {}
1511
+ response.metadata['trace_id'] = trace_id
1512
+
1513
+ return response
1514
+
1518
1515
  async def _agenerate_internal(self,
1519
1516
  prompt: str,
1520
1517
  messages: Optional[List[Dict]],
@@ -1577,4 +1574,4 @@ Please provide a structured response."""
1577
1574
  # Yield chunks asynchronously
1578
1575
  for chunk in sync_gen:
1579
1576
  yield chunk
1580
- await asyncio.sleep(0) # Yield control to event loop
1577
+ await asyncio.sleep(0) # Yield control to event loop
@@ -1956,39 +1956,6 @@ 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
-
1992
1959
  async def process_chat_completion(
1993
1960
  provider: str,
1994
1961
  model: str,
@@ -2052,11 +2019,6 @@ async def process_chat_completion(
2052
2019
  # Create LLM instance
2053
2020
  # Prepare provider-specific kwargs
2054
2021
  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)
2060
2022
  if request.base_url:
2061
2023
  provider_kwargs["base_url"] = request.base_url
2062
2024
  logger.info(
@@ -2085,8 +2047,6 @@ async def process_chat_completion(
2085
2047
  "tool_choice": request.tool_choice if request.tools else None,
2086
2048
  "execute_tools": False, # Server mode - don't execute tools
2087
2049
  }
2088
- if trace_metadata:
2089
- gen_kwargs["trace_metadata"] = trace_metadata
2090
2050
 
2091
2051
  # Add optional parameters
2092
2052
  if request.stop:
@@ -2121,18 +2081,9 @@ async def process_chat_completion(
2121
2081
  )
2122
2082
  else:
2123
2083
  response = llm.generate(**gen_kwargs)
2124
- openai_response = convert_to_openai_response(
2084
+ return convert_to_openai_response(
2125
2085
  response, provider, model, syntax_rewriter, request_id
2126
2086
  )
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
2136
2087
  finally:
2137
2088
  # Cleanup temporary files (base64 and downloaded images) with delay to avoid race conditions
2138
2089
  import threading
@@ -2457,4 +2408,4 @@ Debug Mode:
2457
2408
  # ============================================================================
2458
2409
 
2459
2410
  if __name__ == "__main__":
2460
- run_server_with_args()
2411
+ run_server_with_args()
@@ -4,63 +4,42 @@ 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
- 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
- --------------
7
+ Key components:
24
8
  - Core types (ToolDefinition, ToolCall, ToolResult)
25
9
  - Universal handler for all models
26
10
  - Architecture-based parsing and formatting
27
11
  - Tool registry for managing available tools
28
12
 
29
- Example: Passthrough Mode (Default)
30
- -----------------------------------
13
+ Example usage:
31
14
  ```python
32
- from abstractcore import create_llm
33
- from abstractcore.tools import tool
15
+ from abstractcore.tools import ToolDefinition, UniversalToolHandler, register_tool
34
16
 
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"
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)
38
23
 
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
- ```
24
+ # Register the tool
25
+ tool_def = ToolDefinition.from_function(list_files)
43
26
 
44
- Example: Direct Execution Mode
45
- ------------------------------
46
- ```python
47
- from abstractcore import create_llm
48
- from abstractcore.tools import tool, register_tool
27
+ # Create handler for a model
28
+ handler = UniversalToolHandler("qwen3-coder:30b")
49
29
 
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"
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)
53
35
 
54
- register_tool(get_weather) # Required for direct execution
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")
55
39
 
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
40
+ if tool_calls.has_tool_calls():
41
+ print("Tool calls found:", tool_calls.tool_calls)
59
42
  ```
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.
64
43
  """
65
44
 
66
45
  # 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 and codes using regex (returns file paths with line numbers by default)",
243
+ description="Search for text patterns INSIDE files 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 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.",
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",
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 only first occurrence",
1767
+ "description": "Replace with occurrence limit",
1768
1768
  "arguments": {
1769
1769
  "file_path": "document.txt",
1770
1770
  "pattern": "TODO",
@@ -1780,147 +1780,9 @@ 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
- }
1792
1783
  }
1793
1784
  ]
1794
1785
  )
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
-
1924
1786
  def edit_file(
1925
1787
  file_path: str,
1926
1788
  pattern: str,
@@ -1930,14 +1792,12 @@ def edit_file(
1930
1792
  start_line: Optional[int] = None,
1931
1793
  end_line: Optional[int] = None,
1932
1794
  preview_only: bool = False,
1933
- encoding: str = "utf-8",
1934
- flexible_whitespace: bool = True
1795
+ encoding: str = "utf-8"
1935
1796
  ) -> str:
1936
1797
  """
1937
- Replace text patterns in files using pattern matching.
1798
+ Edit files using pattern matching and replacement.
1938
1799
 
1939
1800
  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.
1941
1801
 
1942
1802
  Args:
1943
1803
  file_path: Path to the file to edit
@@ -1949,10 +1809,6 @@ def edit_file(
1949
1809
  end_line: Ending line number to limit search scope (1-indexed, optional)
1950
1810
  preview_only: Show what would be changed without applying (default: False)
1951
1811
  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).
1956
1812
 
1957
1813
  Returns:
1958
1814
  Success message with replacement details or error message
@@ -1983,10 +1839,6 @@ def edit_file(
1983
1839
 
1984
1840
  original_content = content
1985
1841
 
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
-
1990
1842
  # Handle line range targeting if specified
1991
1843
  search_content = content
1992
1844
  line_offset = 0
@@ -2037,31 +1889,16 @@ def edit_file(
2037
1889
  else:
2038
1890
  # Simple text replacement on search content
2039
1891
  count = search_content.count(pattern)
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:
1892
+ if count == 0:
2055
1893
  range_info = f" (lines {start_line}-{end_line})" if start_line is not None or end_line is not None else ""
2056
1894
  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
2057
1899
  else:
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)
1900
+ updated_search_content = search_content.replace(pattern, replacement, max_replacements)
1901
+ replacements_made = min(count, max_replacements)
2065
1902
 
2066
1903
  # Reconstruct the full file content if line ranges were used
2067
1904
  if start_line is not None or end_line is not None:
@@ -2151,6 +1988,7 @@ def edit_file(
2151
1988
  return f"❌ Error editing file: {str(e)}"
2152
1989
 
2153
1990
 
1991
+
2154
1992
  @tool(
2155
1993
  description="Execute shell commands safely with security controls and platform detection",
2156
1994
  tags=["command", "shell", "execution", "system"],
@@ -148,64 +148,10 @@ 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
-
203
151
  def _get_tool_format(model_name: Optional[str]) -> ToolFormat:
204
152
  """Get tool format for a model."""
205
153
  if not model_name:
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
154
+ return ToolFormat.RAW_JSON
209
155
 
210
156
  architecture = detect_architecture(model_name)
211
157
  arch_format = get_architecture_format(architecture)
@@ -229,10 +175,7 @@ def _get_tool_format(model_name: Optional[str]) -> ToolFormat:
229
175
  def _parse_special_token(response: str) -> List[ToolCall]:
230
176
  """Parse Qwen-style <|tool_call|> format with robust fallback."""
231
177
  tool_calls = []
232
-
233
- # SANITIZE FIRST: Fix malformed tags (doubled tags, broken closing tags)
234
- response = _sanitize_tool_call_tags(response)
235
-
178
+
236
179
  # Pre-process: Remove markdown code fences that might wrap tool calls
237
180
  # This handles cases like ```json\n<|tool_call|>...\n```
238
181
  cleaned_response = re.sub(r'```(?:json|python|tool_code|tool_call)?\s*\n', '', response, flags=re.IGNORECASE)
@@ -307,40 +250,8 @@ def _parse_special_token(response: str) -> List[ToolCall]:
307
250
  try:
308
251
  tool_data = json.loads(json_str)
309
252
  except json.JSONDecodeError:
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)
253
+ # Fallback: fix common LLM JSON issues (unescaped newlines)
254
+ fixed_json = json_str.replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
344
255
  tool_data = json.loads(fixed_json)
345
256
 
346
257
  if isinstance(tool_data, dict):
@@ -513,9 +424,6 @@ def _parse_raw_json(response: str) -> List[ToolCall]:
513
424
 
514
425
  def _parse_any_format(response: str) -> List[ToolCall]:
515
426
  """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
-
519
427
  tool_calls = []
520
428
 
521
429
  # Try each parser and accumulate results
@@ -6,7 +6,6 @@ and executing them safely.
6
6
  """
7
7
 
8
8
  import time
9
- import warnings
10
9
  from typing import Dict, List, Any, Callable, Optional, Union
11
10
  from functools import wraps
12
11
 
@@ -271,12 +270,6 @@ def register_tool(tool: Union[ToolDefinition, Callable]) -> ToolDefinition:
271
270
  Returns:
272
271
  The registered ToolDefinition
273
272
  """
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
- )
280
273
  return _global_registry.register(tool)
281
274
 
282
275
 
@@ -315,11 +308,19 @@ def clear_registry():
315
308
  return _global_registry.clear()
316
309
 
317
310
 
318
- __all__ = [
319
- "ToolRegistry",
320
- "get_registry",
321
- "register_tool",
322
- "execute_tool",
323
- "execute_tools",
324
- "clear_registry",
325
- ]
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
@@ -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.8"
14
+ __version__ = "2.6.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractcore
3
- Version: 2.6.8
3
+ Version: 2.6.9
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>
@@ -195,50 +195,6 @@ response = llm.generate(
195
195
  print(response.content)
196
196
  ```
197
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
-
242
198
  ### Response Object (GenerateResponse)
243
199
 
244
200
  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=d3rC-nqXJ5LDeG0MWEcODcyEqzF4A8FWe27fNcCBBPY,69235
15
+ abstractcore/assets/model_capabilities.json,sha256=tmfQNNPTNJAauF51nPYkMqcjc17F2geeYngh0HjEUZ0,68836
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
@@ -61,15 +61,15 @@ abstractcore/media/processors/pdf_processor.py,sha256=qniYt7cTYYPVRi_cS1IsXztOld
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
63
  abstractcore/media/utils/image_scaler.py,sha256=uUVF91_mLTt-PR0LZiDawTolE2qIrTG1rzV9oYVbxhg,11171
64
- abstractcore/processing/__init__.py,sha256=T9DPd6iLo06opJ2DrUIjmqGhYcvvYNTfuOp0DNEoU5Q,1024
64
+ abstractcore/processing/__init__.py,sha256=QcACEnhnHKYCkFL1LNOW_uqBrwkTAmz5A61N4K2dyu0,988
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=XYAKpsOiqVGt1Rl9K6Uaa3YyANc-zSvqD4qH0KAnffM,28603
69
+ abstractcore/processing/basic_summarizer.py,sha256=XHNxMQ_8aLStTeUo6_2JaThlct12Htpz7ORmm0iuJsg,25495
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=yP75mfBW_SM7IEcMO_Sl-zeNq-IjzJWzYq-ouMOyRQs,69402
72
+ abstractcore/providers/base.py,sha256=spDiRQw-lWHDaqW_4bejxBVDyKO4f4S9JvE97A_gIq8,69199
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=XQueggO5TtkhhxLaVQQ7pxpIPbh4QqB6NcwQ9GJwzAQ,99418
84
+ abstractcore/server/app.py,sha256=AYupb0rlmf4-L9xDb7qHVLdUi8lGC-jYgg1Pe_AvZ3o,97507
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=JPPLRLMnwJorbudHL3sWZrJVKA_IHuZLnOGrpHYliGY,3034
89
- abstractcore/tools/common_tools.py,sha256=zSE5H8PkOQ3SHYi9-srmfnTlkl9jWoKc0TEq9G53ibc,102148
88
+ abstractcore/tools/__init__.py,sha256=oh6vG0RdM1lqUtOp95mLrTsWLh9VmhJf5_FVjGIP5_M,2259
89
+ abstractcore/tools/common_tools.py,sha256=hn4Y-HmpYAH3nLnKqJayWr-mpou3T5Ixu-LRdeO1_c4,95161
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=Otn1rL2JVTN87CYQuLdXlSM1wTW_Juf2PMsDCJpYzuw,30979
93
- abstractcore/tools/registry.py,sha256=2C8zNJqVQwBnmnkWogvnNwCktJIBvdo4lA9W5HxJtm8,9184
92
+ abstractcore/tools/parser.py,sha256=AYM0-baSGGGdwd_w2My3B3sOiw2flE2x59mLmdpwJ0A,27768
93
+ abstractcore/tools/registry.py,sha256=eI0qKrauT1PzLIfaporXCjBMojmWYzgfd96xdN_Db1g,9087
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=BjFWFsKdU0RCZy3o-rumfIgdOnuCfs0EPAMpSfaof5Y,605
103
+ abstractcore/utils/version.py,sha256=LXxKtHI5sV3Fa03LU800IJIuxwTAT0LPpqyGF-nUNao,605
104
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,,
105
+ abstractcore-2.6.9.dist-info/licenses/LICENSE,sha256=PI2v_4HMvd6050uDD_4AY_8PzBnu2asa3RKbdDjowTA,1078
106
+ abstractcore-2.6.9.dist-info/METADATA,sha256=3Ki5l3Fxn3OLZyYSj2RxxFrODunMH-G283lQC2C6iIs,45837
107
+ abstractcore-2.6.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
+ abstractcore-2.6.9.dist-info/entry_points.txt,sha256=jXNdzeltVs23A2JM2e2HOiAHldHrsnud3EvPI5VffOs,658
109
+ abstractcore-2.6.9.dist-info/top_level.txt,sha256=DiNHBI35SIawW3N9Z-z0y6cQYNbXd32pvBkW0RLfScs,13
110
+ abstractcore-2.6.9.dist-info/RECORD,,