diff --git a/CHANGELOG.md b/CHANGELOG.md index acc496f6..ceeb398f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## v0.8.40 - 2025-01-31 + +### Features +- **Autonomous Error Tracking**: Added comprehensive error activity tracking for autonomous task execution. The system now automatically creates agent activities when tasks fail, return empty responses, or encounter unexpected errors, improving error visibility and debugging capabilities. +- **Memory Management**: Added `has_memory` flag support for autonomous tasks, allowing fine-grained control over thread memory persistence per task execution. + +### Bug Fixes +- **Changelog Generation**: Fixed bug in changelog generation process. + +### Technical Details +- Enhanced `run_autonomous_task` function with error detection and activity creation +- Improved error handling for empty responses, system errors, and exceptions +- Added proper logging for error activity creation failures + +**Full Changelog**: https://github.com/crestalnetwork/intentkit/compare/v0.8.39...v0.8.40 + ## v0.8.39 - 2026-01-04 ### Features diff --git a/docker-compose.yml b/docker-compose.yml index 04d8ed38..ae3ca0c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_DB: ${POSTGRES_DB:-intentkit} volumes: - - postgres_data:/var/lib/postgresql/data + - ./pg_data:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] interval: 5s diff --git a/intentkit/core/prompt.py b/intentkit/core/prompt.py index 46722e24..0166802c 100644 --- a/intentkit/core/prompt.py +++ b/intentkit/core/prompt.py @@ -23,10 +23,10 @@ You are integrated with the Enso API. You can use enso_get_tokens to retrieve token information, including APY, Protocol Slug, Symbol, Address, Decimals, and underlying tokens. When interacting with token amounts, -ensure to multiply input amounts by the token's decimal places and divide output amounts by the token's decimals. -Utilize enso_route_shortcut to find the best swap or deposit route. Set broadcast_request to True only when the -user explicitly requests a transaction broadcast. Insufficient funds or insufficient spending approval can cause -Route Shortcut broadcasts to fail. To avoid this, use the enso_broadcast_wallet_approve tool that requires explicit +ensure to multiply input amounts by the token's decimal places and divide output amounts by the token's decimals. +Utilize enso_route_shortcut to find the best swap or deposit route. Set broadcast_request to True only when the +user explicitly requests a transaction broadcast. Insufficient funds or insufficient spending approval can cause +Route Shortcut broadcasts to fail. To avoid this, use the enso_broadcast_wallet_approve tool that requires explicit user confirmation before broadcasting any approval transactions for security reasons. """ @@ -208,6 +208,7 @@ def build_agent_prompt(agent: Agent, agent_data: AgentData) -> str: - Wallet information - Agent characteristics (purpose, personality, principles) - Skills-specific guides + - Extra prompt from template Args: agent: The agent configuration @@ -226,7 +227,13 @@ def build_agent_prompt(agent: Agent, agent_data: AgentData) -> str: _build_skills_guides_section(agent), ] - return "".join(section for section in prompt_sections if section) + base_prompt = "".join(section for section in prompt_sections if section) + + # Add extra_prompt from template if present + if agent.extra_prompt: + base_prompt += f"## Task Details\n\n{agent.extra_prompt}\n\n" + + return base_prompt # Legacy function name for backward compatibility @@ -385,6 +392,8 @@ async def build_system_prompt( if agent.prompt_append: processed_append = await explain_prompt(agent.prompt_append) - final_system_prompt = f"{final_system_prompt}{processed_append}" + final_system_prompt = ( + f"{final_system_prompt}## Additional Instructions\n\n{processed_append}" + ) return final_system_prompt diff --git a/intentkit/core/template.py b/intentkit/core/template.py index fedcf500..fae0c1f7 100644 --- a/intentkit/core/template.py +++ b/intentkit/core/template.py @@ -7,7 +7,7 @@ from pydantic import BaseModel from sqlalchemy import select -from intentkit.models.agent import Agent, AgentCore, AgentTable +from intentkit.models.agent import Agent, AgentCore, AgentTable, AgentVisibility from intentkit.models.db import get_session from intentkit.models.template import Template, TemplateTable @@ -120,6 +120,9 @@ class AgentCreationFromTemplate(BaseModel): name: str | None = None picture: str | None = None description: str | None = None + readonly_wallet_address: str | None = None + weekly_spending_limit: float | None = None + extra_prompt: str | None = None async def create_agent_from_template( @@ -158,6 +161,9 @@ async def create_agent_from_template( if data.picture: core_data["picture"] = data.picture + # Set visibility based on team_id + visibility = AgentVisibility.TEAM if team_id else AgentVisibility.PRIVATE + # Create new agent db_agent = AgentTable( id=str(XID()), @@ -165,6 +171,10 @@ async def create_agent_from_template( team_id=team_id, template_id=data.template_id, description=data.description, + readonly_wallet_address=data.readonly_wallet_address, + weekly_spending_limit=data.weekly_spending_limit, + extra_prompt=data.extra_prompt, + visibility=visibility, **core_data, ) db.add(db_agent) diff --git a/intentkit/models/agent.py b/intentkit/models/agent.py index 3094f364..f15ffd2b 100644 --- a/intentkit/models/agent.py +++ b/intentkit/models/agent.py @@ -8,6 +8,7 @@ import warnings from datetime import UTC, datetime from decimal import Decimal +from enum import IntEnum from pathlib import Path from typing import Annotated, Any, Literal @@ -19,7 +20,7 @@ from pydantic import Field as PydanticField from pydantic.json_schema import SkipJsonSchema from pydantic.main import IncEx -from sqlalchemy import Boolean, DateTime, Float, Numeric, String, func, select +from sqlalchemy import Boolean, DateTime, Float, Integer, Numeric, String, func, select from sqlalchemy.dialects.postgresql import JSON, JSONB from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession @@ -43,6 +44,20 @@ ) +class AgentVisibility(IntEnum): + """Agent visibility levels with hierarchical ordering. + + Higher values indicate broader visibility: + - PRIVATE (0): Only visible to owner + - TEAM (10): Visible to team members + - PUBLIC (20): Visible to everyone + """ + + PRIVATE = 0 + TEAM = 10 + PUBLIC = 20 + + class AgentAutonomous(BaseModel): """Autonomous agent configuration.""" @@ -458,6 +473,17 @@ class AgentTable(Base, AgentUserInputColumns): nullable=True, comment="Price of the x402 request", ) + visibility: Mapped[int | None] = mapped_column( + Integer, + nullable=True, + index=True, + comment="Visibility level: 0=private, 10=team, 20=public", + ) + archived_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), + nullable=True, + comment="Timestamp when the agent was archived. NULL means not archived", + ) # auto timestamp created_at: Mapped[datetime] = mapped_column( @@ -764,6 +790,28 @@ class AgentUpdate(AgentUserInput): }, ), ] + extra_prompt: Annotated[ + str | None, + PydanticField( + default=None, + description="Only when the agent is created from a template.", + max_length=20000, + ), + ] + visibility: Annotated[ + AgentVisibility | None, + PydanticField( + default=None, + description="Visibility level of the agent: PRIVATE(0), TEAM(10), or PUBLIC(20)", + ), + ] + archived_at: Annotated[ + datetime | None, + PydanticField( + default=None, + description="Timestamp when the agent was archived. NULL means not archived", + ), + ] @field_validator("purpose", "personality", "principles", "prompt", "prompt_append") @classmethod @@ -949,14 +997,6 @@ class AgentCreate(AgentUpdate): max_length=50, ), ] - extra_prompt: Annotated[ - str | None, - PydanticField( - default=None, - description="Only when the agent is created from a template.", - max_length=20000, - ), - ] async def check_upstream_id(self) -> None: if not self.upstream_id: @@ -1261,7 +1301,6 @@ class Agent(AgentCreate, AgentPublicInfo): description="Timestamp when the agent public info was last updated", ), ] - # auto timestamp created_at: Annotated[ datetime, diff --git a/tests/core/test_template.py b/tests/core/test_template.py index 642e02f5..2fad55a7 100644 --- a/tests/core/test_template.py +++ b/tests/core/test_template.py @@ -9,7 +9,7 @@ create_template_from_agent, render_agent, ) -from intentkit.models.agent import Agent, AgentTable +from intentkit.models.agent import Agent, AgentTable, AgentVisibility from intentkit.models.template import Template, TemplateTable @@ -76,6 +76,9 @@ async def mock_refresh(instance): assert added_agent.owner == "user_1" assert added_agent.team_id == "team_1" + # Verify visibility is set to TEAM when team_id is provided + assert added_agent.visibility == AgentVisibility.TEAM + # Verify overrides assert added_agent.name == "New Agent" assert added_agent.picture == "new_pic.png" @@ -94,6 +97,67 @@ async def mock_refresh(instance): assert agent.id == added_agent.id +@pytest.mark.asyncio +async def test_create_agent_from_template_without_team(): + """Test creating an agent from a template without team_id (PRIVATE visibility).""" + + # 1. Setup Data + template_id = "template-2" + template_data = { + "id": template_id, + "name": "Template Agent", + "description": "A template agent", + "model": "gpt-4o", + "temperature": 0.5, + "prompt": "You are a template.", + } + + # Mock TemplateTable instance + mock_template = TemplateTable(**template_data) + + # Input data for creation + creation_data = AgentCreationFromTemplate( + template_id=template_id, + name="Private Agent", + picture="private_pic.png", + description="Created without team", + ) + + # 2. Mock Database + with patch("intentkit.core.template.get_session") as mock_get_session: + # Setup mock session + mock_session = MagicMock() + mock_get_session.return_value.__aenter__.return_value = mock_session + + # Mock scalar return value + mock_session.scalar = AsyncMock(return_value=mock_template) + mock_session.commit = AsyncMock() + + # Set timestamps on refresh + async def mock_refresh(instance): + instance.created_at = datetime.now() + instance.updated_at = datetime.now() + + mock_session.refresh = AsyncMock(side_effect=mock_refresh) + + # 3. Call Function without team_id + await create_agent_from_template( + data=creation_data, owner="user_2", team_id=None + ) + + # 4. Verify + assert mock_session.add.called + args, _ = mock_session.add.call_args + added_agent = args[0] + + assert isinstance(added_agent, AgentTable) + assert added_agent.owner == "user_2" + assert added_agent.team_id is None + + # Verify visibility is set to PRIVATE when team_id is None + assert added_agent.visibility == AgentVisibility.PRIVATE + + @pytest.mark.asyncio async def test_create_template_from_agent(): """Test creating a template from an agent."""