Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
11 changes: 11 additions & 0 deletions src/praisonai-agents/praisonaiagents/managed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@
ComputeConfig,
InstanceInfo,
InstanceStatus,
SessionInfo,
)

# Lazy re-export of ManagedBackendProtocol from agent.protocols
# Following AGENTS.md protocol-driven design principles
def __getattr__(name: str):
if name == "ManagedBackendProtocol":
from ..agent.protocols import ManagedBackendProtocol
return ManagedBackendProtocol
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

__all__ = [
"ManagedEvent",
"AgentMessageEvent",
Expand All @@ -41,4 +50,6 @@
"ComputeConfig",
"InstanceInfo",
"InstanceStatus",
"SessionInfo",
"ManagedBackendProtocol", # Lazy re-export from agent.protocols
]
30 changes: 30 additions & 0 deletions src/praisonai-agents/praisonaiagents/managed/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,36 @@ class InstanceInfo:
metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class SessionInfo:
"""Unified session information schema for all managed backends.

Provides a consistent interface for session metadata across
Anthropic Managed Agents and Local Managed Agents.
"""
id: str
status: Optional[str] = None
usage: Optional[Dict[str, int]] = None

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "SessionInfo":
"""Create SessionInfo from backend-specific dict."""
return cls(
id=data["id"],
status=data.get("status"),
usage=data.get("usage")
)

def to_dict(self) -> Dict[str, Any]:
"""Convert to dict format expected by ManagedBackendProtocol."""
result = {"id": self.id}
if self.status is not None:
result["status"] = self.status
if self.usage is not None:
result["usage"] = self.usage
return result


@runtime_checkable
class ComputeProviderProtocol(Protocol):
"""Protocol for compute backends that host managed agent sandboxes.
Expand Down
114 changes: 114 additions & 0 deletions src/praisonai/praisonai/cli/commands/managed.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,32 @@ def managed_multi(
typer.echo(f"Output tokens: {managed.total_output_tokens}")


@sessions_app.command("delete")
def sessions_delete(
session_id: str = typer.Argument(..., help="Session ID to delete (sesn_01...)"),
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
):
"""Delete a managed session.

Example:
praisonai managed sessions delete sesn_01AbCdEf
praisonai managed sessions delete sesn_01AbCdEf --force
"""
if not force:
confirm = typer.confirm(f"Are you sure you want to delete session {session_id}?")
if not confirm:
typer.echo("Cancelled.")
return

try:
client = _get_client()
client.beta.sessions.delete(session_id)
typer.echo(f"Deleted session: {session_id}")
except Exception as e:
typer.echo(f"Error deleting session: {e}", err=True)
raise typer.Exit(1)


# ─────────────────────────────────────────────────────────────────────────────
# sessions sub-commands
# ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -397,6 +423,32 @@ def agents_update(
typer.echo(f"Updated agent: {updated.id} (v{getattr(updated,'version','')})")


@agents_app.command("delete")
def agents_delete(
agent_id: str = typer.Argument(..., help="Agent ID to delete (agent_01...)"),
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
):
"""Delete a managed agent.

Example:
praisonai managed agents delete agent_01AbCdEf
praisonai managed agents delete agent_01AbCdEf --force
"""
if not force:
confirm = typer.confirm(f"Are you sure you want to delete agent {agent_id}?")
if not confirm:
typer.echo("Cancelled.")
return

try:
client = _get_client()
client.beta.agents.delete(agent_id)
typer.echo(f"Deleted agent: {agent_id}")
except Exception as e:
typer.echo(f"Error deleting agent: {e}", err=True)
raise typer.Exit(1)


# ─────────────────────────────────────────────────────────────────────────────
# envs sub-commands
# ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -441,6 +493,68 @@ def envs_get(
typer.echo(f"Config: {cfg}")


@envs_app.command("update")
def envs_update(
env_id: str = typer.Argument(..., help="Environment ID to update (env_01...)"),
name: Optional[str] = typer.Option(None, "--name", "-n", help="New environment name"),
config: Optional[str] = typer.Option(None, "--config", "-c", help="New environment config (JSON)"),
):
"""Update a managed environment.

Example:
praisonai managed envs update env_01AbCdEf --name "Updated Env"
praisonai managed envs update env_01AbCdEf --config '{"packages": ["numpy"]}'
"""
client = _get_client()
kwargs = {}
if name:
kwargs["name"] = name
if config:
import json
try:
kwargs["config"] = json.loads(config)
except json.JSONDecodeError as e:
typer.echo(f"Error parsing config JSON: {e}", err=True)
raise typer.Exit(1)

if not kwargs:
typer.echo("No update fields provided. Use --name or --config.", err=True)
raise typer.Exit(1)

try:
updated = client.beta.environments.update(env_id, **kwargs)
typer.echo(f"Updated environment: {updated.id}")
except Exception as e:
typer.echo(f"Error updating environment: {e}", err=True)
raise typer.Exit(1)


@envs_app.command("delete")
def envs_delete(
env_id: str = typer.Argument(..., help="Environment ID to delete (env_01...)"),
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
):
"""Delete a managed environment.

Example:
praisonai managed envs delete env_01AbCdEf
praisonai managed envs delete env_01AbCdEf --force
"""
if not force:
confirm = typer.confirm(f"Are you sure you want to delete environment {env_id}?")
if not confirm:
typer.echo("Cancelled.")
return

try:
client = _get_client()
client.beta.environments.delete(env_id)
typer.echo(f"Deleted environment: {env_id}")
except Exception as e:
typer.echo(f"Error deleting environment: {e}", err=True)
raise typer.Exit(1)


# ─────────────────────────────────────────────────────────────────────────────
# ids sub-commands (save / restore / show — no Anthropic IDs are user-defined)
# ─────────────────────────────────────────────────────────────────────────────
Expand Down
31 changes: 24 additions & 7 deletions src/praisonai/praisonai/integrations/managed_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,22 +565,39 @@ def interrupt(self) -> None:
# retrieve_session — ManagedBackendProtocol
# ------------------------------------------------------------------
def retrieve_session(self) -> Dict[str, Any]:
"""Retrieve current session metadata and usage from the API."""
"""Retrieve current session metadata and usage from the API using unified schema."""
if not self._session_id:
return {}
client = self._get_client()
sess = client.beta.sessions.retrieve(self._session_id)
result: Dict[str, Any] = {
"id": getattr(sess, "id", self._session_id),
"status": getattr(sess, "status", None),
}

# Build usage dict if available
usage_dict = None
usage = getattr(sess, "usage", None)
if usage:
result["usage"] = {
usage_dict = {
"input_tokens": getattr(usage, "input_tokens", 0),
"output_tokens": getattr(usage, "output_tokens", 0),
}
return result

# Use unified SessionInfo schema for consistency with Local backend
try:
from praisonaiagents.managed import SessionInfo
session_info = SessionInfo(
id=getattr(sess, "id", self._session_id),
status=getattr(sess, "status", None),
usage=usage_dict
)
return session_info.to_dict()
except ImportError:
# Fallback to old format if SessionInfo not available
result: Dict[str, Any] = {
"id": getattr(sess, "id", self._session_id),
"status": getattr(sess, "status", None),
}
if usage_dict:
result["usage"] = usage_dict
return result
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 | 🟡 Minor

Minor: SessionInfo path and fallback return subtly different dicts.

When sess.status is None, SessionInfo.to_dict() omits the status key entirely (see protocols.py lines 112–113), but the except ImportError fallback at lines 594–597 always emits "status": None. Any consumer using result["status"] vs result.get("status") will behave differently depending on which path runs. Given the fallback is dead code in practice (SessionInfo lives in the same core SDK package as this caller), consider dropping the try/except and just using SessionInfo unconditionally, or align the fallback to also omit keys when None.

♻️ Simpler version without fallback
-        # Use unified SessionInfo schema for consistency with Local backend
-        try:
-            from praisonaiagents.managed import SessionInfo
-            session_info = SessionInfo(
-                id=getattr(sess, "id", self._session_id),
-                status=getattr(sess, "status", None),
-                usage=usage_dict
-            )
-            return session_info.to_dict()
-        except ImportError:
-            # Fallback to old format if SessionInfo not available
-            result: Dict[str, Any] = {
-                "id": getattr(sess, "id", self._session_id),
-                "status": getattr(sess, "status", None),
-            }
-            if usage_dict:
-                result["usage"] = usage_dict
-            return result
+        from praisonaiagents.managed import SessionInfo
+        return SessionInfo(
+            id=getattr(sess, "id", self._session_id),
+            status=getattr(sess, "status", None),
+            usage=usage_dict,
+        ).to_dict()
📝 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
# Use unified SessionInfo schema for consistency with Local backend
try:
from praisonaiagents.managed import SessionInfo
session_info = SessionInfo(
id=getattr(sess, "id", self._session_id),
status=getattr(sess, "status", None),
usage=usage_dict
)
return session_info.to_dict()
except ImportError:
# Fallback to old format if SessionInfo not available
result: Dict[str, Any] = {
"id": getattr(sess, "id", self._session_id),
"status": getattr(sess, "status", None),
}
if usage_dict:
result["usage"] = usage_dict
return result
from praisonaiagents.managed import SessionInfo
return SessionInfo(
id=getattr(sess, "id", self._session_id),
status=getattr(sess, "status", None),
usage=usage_dict,
).to_dict()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/praisonai/praisonai/integrations/managed_agents.py` around lines 583 -
600, The current try/except returns inconsistent dict shapes because
SessionInfo.to_dict() omits keys with None while the ImportError fallback always
includes "status": None; fix by removing the fallback and using the unified
SessionInfo path unconditionally: import and construct
praisonaiagents.managed.SessionInfo with id=getattr(sess, "id",
self._session_id), status=getattr(sess, "status", None), usage=usage_dict and
return session_info.to_dict() (i.e., delete the except ImportError block) so the
output shape is consistent; references: SessionInfo, SessionInfo.to_dict, sess,
usage_dict in managed_agents.py.


# ------------------------------------------------------------------
# list_sessions — ManagedBackendProtocol
Expand Down
Loading