Skip to content

Commit 0e86e3b

Browse files
feat: managed agents follow-ups - safety, protocol consistency, CLI parity (fixes #1426 #1429 #1430)
- Add ManagedSandboxRequired exception and host_packages_ok safety flag - Wire compute provider to package installation with sandbox enforcement - Add unified SessionInfo schema with lazy ManagedBackendProtocol re-export - Add missing CLI delete/update commands with confirmation prompts - Maintain protocol-driven architecture and backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
1 parent af1fab4 commit 0e86e3b

File tree

5 files changed

+294
-26
lines changed

5 files changed

+294
-26
lines changed

src/praisonai-agents/praisonaiagents/managed/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,17 @@
2525
ComputeConfig,
2626
InstanceInfo,
2727
InstanceStatus,
28+
SessionInfo,
2829
)
2930

31+
# Lazy re-export of ManagedBackendProtocol from agent.protocols
32+
# Following AGENTS.md protocol-driven design principles
33+
def __getattr__(name: str):
34+
if name == "ManagedBackendProtocol":
35+
from ..agent.protocols import ManagedBackendProtocol
36+
return ManagedBackendProtocol
37+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
38+
3039
__all__ = [
3140
"ManagedEvent",
3241
"AgentMessageEvent",
@@ -41,4 +50,6 @@
4150
"ComputeConfig",
4251
"InstanceInfo",
4352
"InstanceStatus",
53+
"SessionInfo",
54+
"ManagedBackendProtocol", # Lazy re-export from agent.protocols
4455
]

src/praisonai-agents/praisonaiagents/managed/protocols.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,36 @@ class InstanceInfo:
8686
metadata: Dict[str, Any] = field(default_factory=dict)
8787

8888

89+
@dataclass
90+
class SessionInfo:
91+
"""Unified session information schema for all managed backends.
92+
93+
Provides a consistent interface for session metadata across
94+
Anthropic Managed Agents and Local Managed Agents.
95+
"""
96+
id: str
97+
status: Optional[str] = None
98+
usage: Optional[Dict[str, int]] = None
99+
100+
@classmethod
101+
def from_dict(cls, data: Dict[str, Any]) -> "SessionInfo":
102+
"""Create SessionInfo from backend-specific dict."""
103+
return cls(
104+
id=data["id"],
105+
status=data.get("status"),
106+
usage=data.get("usage")
107+
)
108+
109+
def to_dict(self) -> Dict[str, Any]:
110+
"""Convert to dict format expected by ManagedBackendProtocol."""
111+
result = {"id": self.id}
112+
if self.status is not None:
113+
result["status"] = self.status
114+
if self.usage is not None:
115+
result["usage"] = self.usage
116+
return result
117+
118+
89119
@runtime_checkable
90120
class ComputeProviderProtocol(Protocol):
91121
"""Protocol for compute backends that host managed agent sandboxes.

src/praisonai/praisonai/cli/commands/managed.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,32 @@ def managed_multi(
211211
typer.echo(f"Output tokens: {managed.total_output_tokens}")
212212

213213

214+
@sessions_app.command("delete")
215+
def sessions_delete(
216+
session_id: str = typer.Argument(..., help="Session ID to delete (sesn_01...)"),
217+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
218+
):
219+
"""Delete a managed session.
220+
221+
Example:
222+
praisonai managed sessions delete sesn_01AbCdEf
223+
praisonai managed sessions delete sesn_01AbCdEf --force
224+
"""
225+
if not force:
226+
confirm = typer.confirm(f"Are you sure you want to delete session {session_id}?")
227+
if not confirm:
228+
typer.echo("Cancelled.")
229+
return
230+
231+
try:
232+
client = _get_client()
233+
client.beta.sessions.delete(session_id)
234+
typer.echo(f"Deleted session: {session_id}")
235+
except Exception as e:
236+
typer.echo(f"Error deleting session: {e}", err=True)
237+
raise typer.Exit(1)
238+
239+
214240
# ─────────────────────────────────────────────────────────────────────────────
215241
# sessions sub-commands
216242
# ─────────────────────────────────────────────────────────────────────────────
@@ -397,6 +423,32 @@ def agents_update(
397423
typer.echo(f"Updated agent: {updated.id} (v{getattr(updated,'version','')})")
398424

399425

426+
@agents_app.command("delete")
427+
def agents_delete(
428+
agent_id: str = typer.Argument(..., help="Agent ID to delete (agent_01...)"),
429+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
430+
):
431+
"""Delete a managed agent.
432+
433+
Example:
434+
praisonai managed agents delete agent_01AbCdEf
435+
praisonai managed agents delete agent_01AbCdEf --force
436+
"""
437+
if not force:
438+
confirm = typer.confirm(f"Are you sure you want to delete agent {agent_id}?")
439+
if not confirm:
440+
typer.echo("Cancelled.")
441+
return
442+
443+
try:
444+
client = _get_client()
445+
client.beta.agents.delete(agent_id)
446+
typer.echo(f"Deleted agent: {agent_id}")
447+
except Exception as e:
448+
typer.echo(f"Error deleting agent: {e}", err=True)
449+
raise typer.Exit(1)
450+
451+
400452
# ─────────────────────────────────────────────────────────────────────────────
401453
# envs sub-commands
402454
# ─────────────────────────────────────────────────────────────────────────────
@@ -441,6 +493,68 @@ def envs_get(
441493
typer.echo(f"Config: {cfg}")
442494

443495

496+
@envs_app.command("update")
497+
def envs_update(
498+
env_id: str = typer.Argument(..., help="Environment ID to update (env_01...)"),
499+
name: Optional[str] = typer.Option(None, "--name", "-n", help="New environment name"),
500+
config: Optional[str] = typer.Option(None, "--config", "-c", help="New environment config (JSON)"),
501+
):
502+
"""Update a managed environment.
503+
504+
Example:
505+
praisonai managed envs update env_01AbCdEf --name "Updated Env"
506+
praisonai managed envs update env_01AbCdEf --config '{"packages": ["numpy"]}'
507+
"""
508+
client = _get_client()
509+
kwargs = {}
510+
if name:
511+
kwargs["name"] = name
512+
if config:
513+
import json
514+
try:
515+
kwargs["config"] = json.loads(config)
516+
except json.JSONDecodeError as e:
517+
typer.echo(f"Error parsing config JSON: {e}", err=True)
518+
raise typer.Exit(1)
519+
520+
if not kwargs:
521+
typer.echo("No update fields provided. Use --name or --config.", err=True)
522+
raise typer.Exit(1)
523+
524+
try:
525+
updated = client.beta.environments.update(env_id, **kwargs)
526+
typer.echo(f"Updated environment: {updated.id}")
527+
except Exception as e:
528+
typer.echo(f"Error updating environment: {e}", err=True)
529+
raise typer.Exit(1)
530+
531+
532+
@envs_app.command("delete")
533+
def envs_delete(
534+
env_id: str = typer.Argument(..., help="Environment ID to delete (env_01...)"),
535+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
536+
):
537+
"""Delete a managed environment.
538+
539+
Example:
540+
praisonai managed envs delete env_01AbCdEf
541+
praisonai managed envs delete env_01AbCdEf --force
542+
"""
543+
if not force:
544+
confirm = typer.confirm(f"Are you sure you want to delete environment {env_id}?")
545+
if not confirm:
546+
typer.echo("Cancelled.")
547+
return
548+
549+
try:
550+
client = _get_client()
551+
client.beta.environments.delete(env_id)
552+
typer.echo(f"Deleted environment: {env_id}")
553+
except Exception as e:
554+
typer.echo(f"Error deleting environment: {e}", err=True)
555+
raise typer.Exit(1)
556+
557+
444558
# ─────────────────────────────────────────────────────────────────────────────
445559
# ids sub-commands (save / restore / show — no Anthropic IDs are user-defined)
446560
# ─────────────────────────────────────────────────────────────────────────────

src/praisonai/praisonai/integrations/managed_agents.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -565,22 +565,39 @@ def interrupt(self) -> None:
565565
# retrieve_session — ManagedBackendProtocol
566566
# ------------------------------------------------------------------
567567
def retrieve_session(self) -> Dict[str, Any]:
568-
"""Retrieve current session metadata and usage from the API."""
568+
"""Retrieve current session metadata and usage from the API using unified schema."""
569569
if not self._session_id:
570570
return {}
571571
client = self._get_client()
572572
sess = client.beta.sessions.retrieve(self._session_id)
573-
result: Dict[str, Any] = {
574-
"id": getattr(sess, "id", self._session_id),
575-
"status": getattr(sess, "status", None),
576-
}
573+
574+
# Build usage dict if available
575+
usage_dict = None
577576
usage = getattr(sess, "usage", None)
578577
if usage:
579-
result["usage"] = {
578+
usage_dict = {
580579
"input_tokens": getattr(usage, "input_tokens", 0),
581580
"output_tokens": getattr(usage, "output_tokens", 0),
582581
}
583-
return result
582+
583+
# Use unified SessionInfo schema for consistency with Local backend
584+
try:
585+
from praisonaiagents.managed import SessionInfo
586+
session_info = SessionInfo(
587+
id=getattr(sess, "id", self._session_id),
588+
status=getattr(sess, "status", None),
589+
usage=usage_dict
590+
)
591+
return session_info.to_dict()
592+
except ImportError:
593+
# Fallback to old format if SessionInfo not available
594+
result: Dict[str, Any] = {
595+
"id": getattr(sess, "id", self._session_id),
596+
"status": getattr(sess, "status", None),
597+
}
598+
if usage_dict:
599+
result["usage"] = usage_dict
600+
return result
584601

585602
# ------------------------------------------------------------------
586603
# list_sessions — ManagedBackendProtocol

0 commit comments

Comments
 (0)