Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/praisonai-agents/praisonaiagents/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,9 @@ def __init__(
"a dict of ToolSearchConfig fields, or ToolSearchConfig"
)

# Process tool_config and artifact storage (moved from tool_output)
self._artifact_store = None

# ============================================================
# END CONSOLIDATED PARAMS EXTRACTION
# ============================================================
Expand Down Expand Up @@ -1556,6 +1559,14 @@ def __init__(
tool_config=tool_config
)

# Set up artifact store if enabled in tool_config
if _tool_config and _tool_config.enable_artifacts:
from ..context.artifact_store import FileSystemArtifactStore
self._artifact_store = _tool_config.artifact_store or FileSystemArtifactStore(
retention_days=_tool_config.artifact_retention_days,
redact_secrets=_tool_config.redact_secrets
)

# Gap 2: Store parallel tool calls setting for ToolCallExecutor selection
self.parallel_tool_calls = _tool_config.parallel if _tool_config else parallel_tool_calls
# G2: Store interrupt controller for cooperative cancellation
Expand Down Expand Up @@ -1779,7 +1790,8 @@ def __init__(
self._init_message_steering(message_steering)
self.verbose = verbose
self._has_explicit_output_config = _has_explicit_output # Track if user set output mode
self.tool_output_limit = tool_output_limit # Configurable tool output limit
# Use tool_config output_limit if configured, otherwise use parameter value
self.tool_output_limit = _tool_config.output_limit if _tool_config else tool_output_limit
self.allow_delegation = allow_delegation
self.step_callback = step_callback
# Token budget guard (zero overhead when _max_budget is None)
Expand Down Expand Up @@ -2197,7 +2209,6 @@ def clone_for_channel(self) -> "Agent":
'approval': getattr(self, '_approval_config', None),
'learn': getattr(self, '_learn_config', None),
'tool_search': getattr(self, '_tool_search_config', None),

# Tool configuration - use consolidated config when available
'tool_config': getattr(self, '_tool_config', None),

Expand Down Expand Up @@ -5593,6 +5604,18 @@ def __del__(self):
memory = getattr(self, "_memory_instance", None)
if memory and hasattr(memory, 'close_connections'):
memory.close_connections()

# Clean up old artifacts if artifact store is configured
artifact_store = getattr(self, "_artifact_store", None)
if artifact_store and hasattr(artifact_store, 'cleanup_old_artifacts'):
try:
artifact_store.cleanup_old_artifacts()
except Exception as e:
# Log the error for debugging but don't fail cleanup
import logging
logging.debug(
f"Failed to cleanup artifacts for agent {self.name}: {e}"
)
except Exception as exc: # noqa: BLE001 - finalizers must not raise
import contextlib
with contextlib.suppress(Exception):
Expand Down
118 changes: 91 additions & 27 deletions src/praisonai-agents/praisonaiagents/agent/tool_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@ def delay(attempt: int, initial_delay: float, backoff_factor: float, jitter: flo

class ToolExecutionMixin:
"""Mixin providing toolexecution methods for the Agent class."""

def _register_artifact_tools(self):
"""Register artifact retrieval tools when artifacts are first created."""
try:
from ..tools import artifact_tools

# Set the store reference for the tools
artifact_tools.set_artifact_store(self._artifact_store)

# Add the retrieval tools
tools_to_add = [
artifact_tools.artifact_head,
artifact_tools.artifact_tail,
artifact_tools.artifact_grep,
artifact_tools.artifact_chunk,
artifact_tools.artifact_load,
artifact_tools.artifact_list,
]
Comment on lines +64 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Register all artifact retrieval tools, not just a subset.

artifact_load and artifact_list exist in artifact_tools.py but are never appended here, so agents can’t access full-content load or listing after spill.

➕ Suggested fix
             tools_to_add = [
                 artifact_tools.artifact_head,
                 artifact_tools.artifact_tail,
                 artifact_tools.artifact_grep,
                 artifact_tools.artifact_chunk,
+                artifact_tools.artifact_load,
+                artifact_tools.artifact_list,
             ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tools_to_add = [
artifact_tools.artifact_head,
artifact_tools.artifact_tail,
artifact_tools.artifact_grep,
artifact_tools.artifact_chunk,
]
tools_to_add = [
artifact_tools.artifact_head,
artifact_tools.artifact_tail,
artifact_tools.artifact_grep,
artifact_tools.artifact_chunk,
artifact_tools.artifact_load,
artifact_tools.artifact_list,
]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/agent/tool_execution.py` around lines 39
- 44, The tools_to_add list in the tool_execution block is incomplete and
excludes artifact_load and artifact_list even though these tools are available
in artifact_tools.py. Add artifact_tools.artifact_load and
artifact_tools.artifact_list to the tools_to_add list alongside the existing
artifact_head, artifact_tail, artifact_grep, and artifact_chunk entries to
ensure agents have access to all available artifact retrieval tools, including
full-content load and listing functionality.


# Only add if not already present
existing_tool_names = {getattr(t, '__name__', str(t)) for t in self.tools}
for tool in tools_to_add:
tool_name = getattr(tool, '__name__', str(tool))
if tool_name not in existing_tool_names:
self.tools.append(tool)

logging.debug("Registered artifact retrieval tools")
except Exception as e:
logging.warning(f"Failed to register artifact tools: {e}")

def _get_existing_stream_emitter(self):
"""Return an already-initialized stream emitter without creating one."""
Expand Down Expand Up @@ -462,43 +491,78 @@ def execute_with_context():
try:
result_str = str(result)

if self.context_manager:
# Use context-aware truncation with configured budget
truncated = self._truncate_tool_output(function_name, result_str, tool_call_id)
else:
# Apply default limit even without context management
# This prevents runaway tool outputs from causing overflow
limit = getattr(self, 'tool_output_limit', 16000)
if len(result_str) > limit:
# Use smart truncation format that judge recognizes as OK
tail_size = min(limit // 5, 2000)
head = result_str[:limit - tail_size]
tail = result_str[-tail_size:] if tail_size > 0 else ""
truncated = f"{head}\n...[{len(result_str):,} chars, showing first/last portions]...\n{tail}"

# Store full output for later retrieval
# Get configured limit
limit = getattr(self, 'tool_output_limit', 16000)

# Check if we need to spill to artifact store
if len(result_str) > limit:
# Try to use artifact store if available
artifact_ref = None
Comment thread
greptile-apps[bot] marked this conversation as resolved.
if hasattr(self, '_artifact_store') and self._artifact_store is not None:
try:
from ..runtime.tool_output_store import get_tool_output_store
store = get_tool_output_store(getattr(self, '_run_id', None))
metadata = store.store(function_name, result_str, call_id=tool_call_id)
if metadata:
# Add reference to stored output in the truncated preview
truncated = store.format_reference(metadata, truncated)
logging.debug(f"Stored full {function_name} output ({len(result_str)} bytes) at {metadata.get('path')}")
except ImportError:
# Fallback if store not available
pass
from ..context.artifacts import ArtifactMetadata

# Create metadata for this artifact
metadata = ArtifactMetadata(
agent_id=self.name,
run_id=getattr(self, '_current_run_id', 'unknown'),
tool_name=function_name,
turn_id=getattr(self, '_turn_counter', 0),
)

# Store the full output
artifact_ref = self._artifact_store.store(result_str, metadata)
logging.debug(f"Stored {function_name} output ({len(result_str)} bytes) as artifact {artifact_ref.artifact_id}")

# Register artifact retrieval tools if not already registered
if not hasattr(self, '_artifact_tools_registered'):
self._register_artifact_tools()
self._artifact_tools_registered = True
except Exception as e:
logging.debug(f"Failed to store tool output: {e}")
logging.debug(f"Failed to store artifact: {e}")

# Generate truncated preview
tail_size = min(limit // 5, 2000)
head = result_str[:limit - tail_size]
tail = result_str[-tail_size:] if tail_size > 0 else ""

# If we stored an artifact, include reference in the output
if artifact_ref:
truncated = (
f"{head}\n"
f"...[{len(result_str):,} chars total, showing first/last portions]...\n"
f"{tail}\n\n"
f"{artifact_ref.to_inline()}"
)
else:
# Fallback to simple truncation
truncated = f"{head}\n...[{len(result_str):,} chars, showing first/last portions]...\n{tail}"
else:
truncated = result_str

if self.context_manager and hasattr(self, '_truncate_tool_output'):
# Use context-aware truncation if available, but preserve artifact reference
if 'artifact_ref' in locals() and artifact_ref:
# Extract the artifact reference from the truncated string
artifact_inline = artifact_ref.to_inline()
# Remove the artifact reference before context truncation
truncated_without_ref = truncated.replace(artifact_inline, "").rstrip()
# Apply context truncation
truncated_without_ref = self._truncate_tool_output(function_name, truncated_without_ref, tool_call_id)
# Re-append the artifact reference
truncated = f"{truncated_without_ref}\n\n{artifact_inline}"
else:
truncated = result_str
truncated = self._truncate_tool_output(function_name, truncated, tool_call_id)

if len(truncated) < len(result_str):
logging.debug(f"Truncated {function_name} output from {len(result_str)} to {len(truncated)} chars")
# For dicts, truncate large string fields (e.g., raw_content from search)
if isinstance(result, dict):
max_field_chars = getattr(self, 'tool_output_limit', 16000) if not self.context_manager else None
result = self._truncate_dict_fields(result, function_name, max_field_chars, tool_call_id)
# Add artifact reference to dict result if available
if 'artifact_ref' in locals() and artifact_ref:
result["_artifact_ref"] = artifact_ref.to_dict()
else:
result = truncated
except Exception as e:
Expand Down
50 changes: 47 additions & 3 deletions src/praisonai-agents/praisonaiagents/config/feature_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..compaction.strategy import CompactionStrategy
from ..context.artifact_store import FileSystemArtifactStore

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move the concrete artifact-store import out of module scope.

Line [45] pulls a concrete filesystem implementation into a core config module at import time. This tightens coupling and adds unnecessary import-path work even when artifact spilling is disabled. Keep this as a lazy/runtime import in the wiring path instead.

As per coding guidelines, "Core SDK (praisonaiagents) must use Protocol-Driven Core architecture with only protocols, hooks, adapters, base classes, and decorators - no heavy implementations" and "All optional dependencies must use lazy imports - never import heavy dependencies at module level; instead import inside functions to avoid import-time performance impact".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/config/feature_configs.py` at line 45,
Remove the import of FileSystemArtifactStore from module-level scope (line 45 in
feature_configs.py) and convert it to a lazy import instead. Identify the
function or method where FileSystemArtifactStore is actually instantiated or
used, and move the import statement inside that function/method. This ensures
that the concrete filesystem implementation is only loaded at runtime when
actually needed, keeping the core config module free of heavy implementations
and maintaining protocol-driven architecture as per coding guidelines.

Source: Coding guidelines



class MemoryBackend(str, Enum):
Expand Down Expand Up @@ -935,7 +936,8 @@ class ToolConfig:
"""
Configuration for tool execution behavior.

Configuration for tool execution behavior including timeout, retry policy, and parallel execution.
Configuration for tool execution behavior including timeout, retry policy, parallel execution,
and artifact storage for large outputs.

Usage:
# Simple enable with defaults
Expand All @@ -948,8 +950,12 @@ class ToolConfig:
parallel=True,
))

# With timeout only
Agent(tool_config=ToolConfig(timeout=30))
# With artifact storage for large outputs
Agent(tool_config=ToolConfig(
output_limit=32000,
enable_artifacts=True,
artifact_retention_days=14,
))
"""
# Tool execution timeout in seconds
timeout: Optional[int] = None
Expand All @@ -960,6 +966,26 @@ class ToolConfig:
# Enable parallel execution of batched LLM tool calls
parallel: bool = False

# Tool output handling and artifact storage
output_limit: int = 16000 # Maximum bytes before spilling to artifact store
output_max_lines: Optional[int] = None # Maximum lines before spilling
output_direction: str = "both" # Truncation direction: "head", "tail", or "both"
enable_artifacts: bool = False # Whether to enable artifact storage (default False for backward compat)
artifact_retention_days: int = 7 # Days to retain artifacts before garbage collection
artifact_store: Optional[Any] = None # Custom artifact store instance
redact_secrets: bool = True # Whether to redact secrets from artifacts

def __post_init__(self) -> None:
"""Validate configuration after initialization."""
if self.output_limit <= 0:
raise ValueError("tool_config.output_limit must be > 0")
if self.output_max_lines is not None and self.output_max_lines <= 0:
raise ValueError("tool_config.output_max_lines must be > 0 when provided")
if self.output_direction not in {"head", "tail", "both"}:
raise ValueError("tool_config.output_direction must be one of: 'head', 'tail', 'both'")
if self.artifact_retention_days < 0:
raise ValueError("tool_config.artifact_retention_days must be >= 0")

def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return {
Expand All @@ -970,6 +996,13 @@ def to_dict(self) -> Dict[str, Any]:
else self.retry_policy
),
"parallel": self.parallel,
"output_limit": self.output_limit,
"output_max_lines": self.output_max_lines,
"output_direction": self.output_direction,
"enable_artifacts": self.enable_artifacts,
"artifact_retention_days": self.artifact_retention_days,
"artifact_store": self.artifact_store,
"redact_secrets": self.redact_secrets,
}

@classmethod
Expand All @@ -988,6 +1021,13 @@ def from_dict(cls, data: Dict[str, Any]) -> "ToolConfig":
timeout=data.get("timeout"),
retry_policy=retry_policy,
parallel=data.get("parallel", False),
output_limit=data.get("output_limit", 16000),
output_max_lines=data.get("output_max_lines"),
output_direction=data.get("output_direction", "both"),
enable_artifacts=data.get("enable_artifacts", False),
artifact_retention_days=data.get("artifact_retention_days", 7),
artifact_store=data.get("artifact_store"),
redact_secrets=data.get("redact_secrets", True),
)


Expand Down Expand Up @@ -1513,6 +1553,8 @@ def resolve_tools(value: ToolParam) -> Optional[ToolConfig]:
return value




__all__ = [
# Enums
"MemoryBackend",
Expand Down Expand Up @@ -1562,6 +1604,7 @@ def resolve_tools(value: ToolParam) -> Optional[ToolConfig]:
"AutonomyParam",
"ToolSearchParam",
"ToolParam",
"ToolOutputParam",
# Precedence ladder resolvers
"resolve_memory",
"resolve_knowledge",
Expand All @@ -1573,4 +1616,5 @@ def resolve_tools(value: ToolParam) -> Optional[ToolConfig]:
"resolve_autonomy",
"resolve_tool_search",
"resolve_tools",
"resolve_tool_output",
Comment on lines 1607 to +1619

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 ToolOutputParam and resolve_tool_output exported without being defined

Both "ToolOutputParam" (line 1607) and "resolve_tool_output" (line 1619) are added to __all__ in this PR but neither name is defined anywhere in the module. A from praisonaiagents.config.feature_configs import * will raise AttributeError: module has no attribute 'ToolOutputParam'; an explicit from ... import ToolOutputParam will raise ImportError. Any downstream code or test that imports these names (which the __all__ declaration implicitly advertises as public API) will break on import.

]
6 changes: 6 additions & 0 deletions src/praisonai-agents/praisonaiagents/context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ def format_percent(value: float) -> str:
"TerminalLoggerProtocol",
"compute_checksum",
"generate_summary",
"FileSystemArtifactStore",
# Context Compaction Policy
"ContextCompactionPolicy",
"ContextCompactionPolicyProtocol",
Expand Down Expand Up @@ -295,6 +296,11 @@ def __getattr__(name: str):
from . import artifacts
return getattr(artifacts, name)

# FileSystemArtifactStore
if name == "FileSystemArtifactStore":
from .artifact_store import FileSystemArtifactStore
return FileSystemArtifactStore

# Session Context Tracking (Agno pattern)
if name in ("SessionContextTracker", "SessionState"):
from . import session_tracker
Expand Down
Loading
Loading