connectonion 0.5.8__tar.gz → 0.5.10__tar.gz

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.
Files changed (134) hide show
  1. {connectonion-0.5.8 → connectonion-0.5.10}/PKG-INFO +1 -1
  2. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/__init__.py +3 -1
  3. connectonion-0.5.10/connectonion/cli/commands/copy_commands.py +116 -0
  4. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/main.py +14 -1
  5. connectonion-0.5.10/connectonion/connect.py +272 -0
  6. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/host.py +31 -14
  7. connectonion-0.5.10/connectonion/transcribe.py +245 -0
  8. {connectonion-0.5.8 → connectonion-0.5.10}/docs/cli/README.md +47 -0
  9. {connectonion-0.5.8 → connectonion-0.5.10}/docs/useful_plugins/README.md +24 -0
  10. {connectonion-0.5.8 → connectonion-0.5.10}/docs/useful_tools/README.md +24 -0
  11. {connectonion-0.5.8 → connectonion-0.5.10}/pyproject.toml +1 -1
  12. connectonion-0.5.8/connectonion/connect.py +0 -128
  13. {connectonion-0.5.8 → connectonion-0.5.10}/.gitignore +0 -0
  14. {connectonion-0.5.8 → connectonion-0.5.10}/README.md +0 -0
  15. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/address.py +0 -0
  16. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/agent.py +0 -0
  17. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/announce.py +0 -0
  18. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/asgi.py +0 -0
  19. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/auto_debug_exception.py +0 -0
  20. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/__init__.py +0 -0
  21. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/browser_agent/__init__.py +0 -0
  22. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/browser_agent/browser.py +0 -0
  23. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/browser_agent/prompt.md +0 -0
  24. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/__init__.py +0 -0
  25. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/auth_commands.py +0 -0
  26. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/browser_commands.py +0 -0
  27. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/create.py +0 -0
  28. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/deploy_commands.py +0 -0
  29. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/doctor_commands.py +0 -0
  30. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/init.py +0 -0
  31. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/project_cmd_lib.py +0 -0
  32. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/reset_commands.py +0 -0
  33. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/status_commands.py +0 -0
  34. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +0 -0
  35. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/docs/connectonion.md +0 -0
  36. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/docs.md +0 -0
  37. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/README.md +0 -0
  38. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/agent.py +0 -0
  39. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +0 -0
  40. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +0 -0
  41. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/prompts/metagent.md +0 -0
  42. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/prompts/think_prompt.md +0 -0
  43. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/minimal/README.md +0 -0
  44. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/minimal/agent.py +0 -0
  45. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/playwright/README.md +0 -0
  46. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/playwright/agent.py +0 -0
  47. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/playwright/prompt.md +0 -0
  48. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/playwright/requirements.txt +0 -0
  49. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/web-research/agent.py +0 -0
  50. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/console.py +0 -0
  51. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_agent/__init__.py +0 -0
  52. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_agent/agent.py +0 -0
  53. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_agent/prompts/debug_assistant.md +0 -0
  54. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_agent/runtime_inspector.py +0 -0
  55. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/__init__.py +0 -0
  56. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/explain_agent.py +0 -0
  57. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/explain_context.py +0 -0
  58. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/explainer_prompt.md +0 -0
  59. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/root_cause_analysis_prompt.md +0 -0
  60. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debugger_ui.py +0 -0
  61. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/decorators.py +0 -0
  62. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/events.py +0 -0
  63. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/execution_analyzer/__init__.py +0 -0
  64. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/execution_analyzer/execution_analysis.py +0 -0
  65. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/execution_analyzer/execution_analysis_prompt.md +0 -0
  66. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/interactive_debugger.py +0 -0
  67. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/llm.py +0 -0
  68. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/llm_do.py +0 -0
  69. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/logger.py +0 -0
  70. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/__init__.py +0 -0
  71. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/analyze_contact.md +0 -0
  72. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/eval_expected.md +0 -0
  73. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/react_evaluate.md +0 -0
  74. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/react_plan.md +0 -0
  75. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/reflect.md +0 -0
  76. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompts.py +0 -0
  77. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/relay.py +0 -0
  78. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/static/docs.html +0 -0
  79. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tool_executor.py +0 -0
  80. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tool_factory.py +0 -0
  81. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tool_registry.py +0 -0
  82. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/trust.py +0 -0
  83. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/trust_agents.py +0 -0
  84. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/trust_functions.py +0 -0
  85. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/__init__.py +0 -0
  86. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/divider.py +0 -0
  87. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/dropdown.py +0 -0
  88. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/footer.py +0 -0
  89. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/fuzzy.py +0 -0
  90. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/input.py +0 -0
  91. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/keys.py +0 -0
  92. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/pick.py +0 -0
  93. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/providers.py +0 -0
  94. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/status_bar.py +0 -0
  95. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/usage.py +0 -0
  96. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_events_handlers/__init__.py +0 -0
  97. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_events_handlers/reflect.py +0 -0
  98. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/__init__.py +0 -0
  99. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/calendar_plugin.py +0 -0
  100. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/eval.py +0 -0
  101. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/gmail_plugin.py +0 -0
  102. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/image_result_formatter.py +0 -0
  103. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/re_act.py +0 -0
  104. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/shell_approval.py +0 -0
  105. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/__init__.py +0 -0
  106. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/diff_writer.py +0 -0
  107. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/get_emails.py +0 -0
  108. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/gmail.py +0 -0
  109. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/google_calendar.py +0 -0
  110. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/memory.py +0 -0
  111. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/microsoft_calendar.py +0 -0
  112. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/outlook.py +0 -0
  113. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/send_email.py +0 -0
  114. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/shell.py +0 -0
  115. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/slash_command.py +0 -0
  116. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/terminal.py +0 -0
  117. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/todo_list.py +0 -0
  118. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/web_fetch.py +0 -0
  119. {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/xray.py +0 -0
  120. {connectonion-0.5.8 → connectonion-0.5.10}/docs/README.md +0 -0
  121. {connectonion-0.5.8 → connectonion-0.5.10}/docs/debug/README.md +0 -0
  122. {connectonion-0.5.8 → connectonion-0.5.10}/docs/integrations/README.md +0 -0
  123. {connectonion-0.5.8 → connectonion-0.5.10}/docs/network/README.md +0 -0
  124. {connectonion-0.5.8 → connectonion-0.5.10}/docs/templates/README.md +0 -0
  125. {connectonion-0.5.8 → connectonion-0.5.10}/docs/tui/README.md +0 -0
  126. {connectonion-0.5.8 → connectonion-0.5.10}/examples/README.md +0 -0
  127. {connectonion-0.5.8 → connectonion-0.5.10}/examples/browser-agent/README.md +0 -0
  128. {connectonion-0.5.8 → connectonion-0.5.10}/examples/email-agent/README.md +0 -0
  129. {connectonion-0.5.8 → connectonion-0.5.10}/examples/simple-agent/README.md +0 -0
  130. {connectonion-0.5.8 → connectonion-0.5.10}/prompts/README.md +0 -0
  131. {connectonion-0.5.8 → connectonion-0.5.10}/prompts/formats/README.md +0 -0
  132. {connectonion-0.5.8 → connectonion-0.5.10}/tests/README.md +0 -0
  133. {connectonion-0.5.8 → connectonion-0.5.10}/tests/cli/README.md +0 -0
  134. {connectonion-0.5.8 → connectonion-0.5.10}/tests/cli/aws/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: connectonion
3
- Version: 0.5.8
3
+ Version: 0.5.10
4
4
  Summary: A simple Python framework for creating AI agents with behavior tracking
5
5
  Project-URL: Homepage, https://github.com/openonion/connectonion
6
6
  Project-URL: Documentation, https://docs.connectonion.com
@@ -1,6 +1,6 @@
1
1
  """ConnectOnion - A simple agent framework with behavior tracking."""
2
2
 
3
- __version__ = "0.5.8"
3
+ __version__ = "0.5.10"
4
4
 
5
5
  # Auto-load .env files for the entire framework
6
6
  from dotenv import load_dotenv
@@ -15,6 +15,7 @@ from .tool_factory import create_tool_from_function
15
15
  from .llm import LLM
16
16
  from .logger import Logger
17
17
  from .llm_do import llm_do
18
+ from .transcribe import transcribe
18
19
  from .prompts import load_system_prompt
19
20
  from .xray import xray
20
21
  from .decorators import replay, xray_replay
@@ -40,6 +41,7 @@ __all__ = [
40
41
  "Logger",
41
42
  "create_tool_from_function",
42
43
  "llm_do",
44
+ "transcribe",
43
45
  "load_system_prompt",
44
46
  "xray",
45
47
  "replay",
@@ -0,0 +1,116 @@
1
+ """
2
+ Purpose: CLI command to copy built-in tools and plugins to user's project for customization
3
+ LLM-Note:
4
+ Dependencies: imports from [shutil, pathlib, typing, rich] | imported by [cli/main.py via handle_copy()]
5
+ Data flow: user runs `co copy <name>` → looks up name in TOOLS/PLUGINS registry → finds source via module.__file__ → copies to ./tools/ or ./plugins/
6
+ State/Effects: creates tools/ or plugins/ directory if needed | copies .py files from installed package to user's project
7
+ Integration: exposes handle_copy() for CLI | uses Python import system to find installed package location (cross-platform)
8
+ """
9
+
10
+ import shutil
11
+ from pathlib import Path
12
+ from typing import Optional, List
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+ console = Console()
17
+
18
+ # Registry of copyable tools
19
+ TOOLS = {
20
+ "gmail": "gmail.py",
21
+ "outlook": "outlook.py",
22
+ "google_calendar": "google_calendar.py",
23
+ "microsoft_calendar": "microsoft_calendar.py",
24
+ "memory": "memory.py",
25
+ "web_fetch": "web_fetch.py",
26
+ "shell": "shell.py",
27
+ "diff_writer": "diff_writer.py",
28
+ "todo_list": "todo_list.py",
29
+ "slash_command": "slash_command.py",
30
+ }
31
+
32
+ # Registry of copyable plugins
33
+ PLUGINS = {
34
+ "re_act": "re_act.py",
35
+ "eval": "eval.py",
36
+ "image_result_formatter": "image_result_formatter.py",
37
+ "shell_approval": "shell_approval.py",
38
+ "gmail_plugin": "gmail_plugin.py",
39
+ "calendar_plugin": "calendar_plugin.py",
40
+ }
41
+
42
+
43
+ def handle_copy(
44
+ names: List[str],
45
+ list_all: bool = False,
46
+ path: Optional[str] = None,
47
+ force: bool = False
48
+ ):
49
+ """Copy built-in tools and plugins to user's project."""
50
+
51
+ # Show list if requested or no names provided
52
+ if list_all or not names:
53
+ show_available_items()
54
+ return
55
+
56
+ # Get source directories using import system (works for installed packages)
57
+ import connectonion.useful_tools as tools_module
58
+ import connectonion.useful_plugins as plugins_module
59
+
60
+ useful_tools_dir = Path(tools_module.__file__).parent
61
+ useful_plugins_dir = Path(plugins_module.__file__).parent
62
+
63
+ current_dir = Path.cwd()
64
+
65
+ for name in names:
66
+ name_lower = name.lower()
67
+
68
+ # Check if it's a tool
69
+ if name_lower in TOOLS:
70
+ source = useful_tools_dir / TOOLS[name_lower]
71
+ dest_dir = Path(path) if path else current_dir / "tools"
72
+ copy_file(source, dest_dir, force)
73
+
74
+ # Check if it's a plugin
75
+ elif name_lower in PLUGINS:
76
+ source = useful_plugins_dir / PLUGINS[name_lower]
77
+ dest_dir = Path(path) if path else current_dir / "plugins"
78
+ copy_file(source, dest_dir, force)
79
+
80
+ else:
81
+ console.print(f"[red]Unknown: {name}[/red]")
82
+ console.print("Use [cyan]co copy --list[/cyan] to see available items")
83
+
84
+
85
+ def copy_file(source: Path, dest_dir: Path, force: bool):
86
+ """Copy a single file to destination."""
87
+ if not source.exists():
88
+ console.print(f"[red]Source not found: {source}[/red]")
89
+ return
90
+
91
+ dest_dir.mkdir(parents=True, exist_ok=True)
92
+ dest = dest_dir / source.name
93
+
94
+ if dest.exists() and not force:
95
+ console.print(f"[yellow]Skipped: {dest} (exists, use --force)[/yellow]")
96
+ return
97
+
98
+ shutil.copy2(source, dest)
99
+ console.print(f"[green]✓ Copied: {dest}[/green]")
100
+
101
+
102
+ def show_available_items():
103
+ """Display available tools and plugins."""
104
+ table = Table(title="Available Items to Copy")
105
+ table.add_column("Name", style="cyan")
106
+ table.add_column("Type", style="green")
107
+ table.add_column("File")
108
+
109
+ for name, file in sorted(TOOLS.items()):
110
+ table.add_row(name, "tool", file)
111
+
112
+ for name, file in sorted(PLUGINS.items()):
113
+ table.add_row(name, "plugin", file)
114
+
115
+ console.print(table)
116
+ console.print("\n[dim]Usage: co copy <name> [--path ./custom/][/dim]")
@@ -11,7 +11,7 @@ LLM-Note:
11
11
 
12
12
  import typer
13
13
  from rich.console import Console
14
- from typing import Optional
14
+ from typing import Optional, List
15
15
 
16
16
  from .. import __version__
17
17
 
@@ -54,6 +54,7 @@ def _show_help():
54
54
  console.print("[bold]Commands:[/bold]")
55
55
  console.print(" [green]create[/green] <name> Create new project")
56
56
  console.print(" [green]init[/green] Initialize in current directory")
57
+ console.print(" [green]copy[/green] <name> Copy tool/plugin source to project")
57
58
  console.print(" [green]deploy[/green] Deploy to ConnectOnion Cloud")
58
59
  console.print(" [green]auth[/green] Authenticate for managed keys")
59
60
  console.print(" [green]status[/green] Check account balance")
@@ -139,6 +140,18 @@ def browser(command: str = typer.Argument(..., help="Browser command")):
139
140
  handle_browser(command)
140
141
 
141
142
 
143
+ @app.command()
144
+ def copy(
145
+ names: List[str] = typer.Argument(None, help="Tool or plugin names to copy"),
146
+ list_all: bool = typer.Option(False, "--list", "-l", help="List available items"),
147
+ path: Optional[str] = typer.Option(None, "--path", "-p", help="Custom destination path"),
148
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
149
+ ):
150
+ """Copy built-in tools/plugins to customize."""
151
+ from .commands.copy_commands import handle_copy
152
+ handle_copy(names=names or [], list_all=list_all, path=path, force=force)
153
+
154
+
142
155
  def cli():
143
156
  """Entry point."""
144
157
  app()
@@ -0,0 +1,272 @@
1
+ """
2
+ Purpose: Client interface for connecting to remote agents via HTTP or relay network
3
+ LLM-Note:
4
+ Dependencies: imports from [asyncio, json, uuid, time, aiohttp, websockets, address] | imported by [__init__.py, tests/test_connect.py, examples/] | tested by [tests/test_connect.py]
5
+ Data flow: connect(address, keys) → RemoteAgent → input() → discover endpoints → try HTTP first → fallback to relay → return result
6
+ State/Effects: caches discovered endpoint for reuse | optional signing with keys parameter
7
+ Integration: exposes connect(address, keys, relay_url), RemoteAgent class with .input(), .input_async()
8
+ Performance: discovery cached per RemoteAgent instance | HTTPS tried first (direct), relay as fallback
9
+
10
+ Connect to remote agents on the network.
11
+
12
+ Smart discovery: tries HTTP endpoints first, falls back to relay.
13
+ Always signs requests when keys are provided.
14
+ """
15
+
16
+ import asyncio
17
+ import json
18
+ import time
19
+ import uuid
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from . import address as addr
23
+
24
+
25
+ class RemoteAgent:
26
+ """
27
+ Interface to a remote agent.
28
+
29
+ Supports:
30
+ - Discovery via relay API
31
+ - Direct HTTP POST to agent /input endpoint
32
+ - WebSocket relay fallback
33
+ - Signed requests when keys provided
34
+ - Multi-turn conversations via session management
35
+
36
+ Usage:
37
+ # Standard Python scripts
38
+ agent = connect("0x...")
39
+ result = agent.input("Hello")
40
+
41
+ # Jupyter notebooks or async code
42
+ agent = connect("0x...")
43
+ result = await agent.input_async("Hello")
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ agent_address: str,
49
+ *,
50
+ keys: Optional[Dict[str, Any]] = None,
51
+ relay_url: str = "wss://oo.openonion.ai/ws/announce"
52
+ ):
53
+ self.address = agent_address
54
+ self._keys = keys
55
+ self._relay_url = relay_url
56
+ self._cached_endpoint: Optional[str] = None
57
+ self._session: Optional[Dict[str, Any]] = None # Multi-turn conversation state
58
+
59
+ def input(self, prompt: str, timeout: float = 30.0) -> str:
60
+ """
61
+ Send task to remote agent and get response (sync version).
62
+
63
+ Automatically maintains conversation context across calls.
64
+
65
+ Note:
66
+ This method cannot be used inside an async context (e.g., Jupyter notebooks,
67
+ async functions). Use input_async() instead in those environments.
68
+
69
+ Args:
70
+ prompt: Task/prompt to send
71
+ timeout: Seconds to wait for response (default 30)
72
+
73
+ Returns:
74
+ Agent's response string
75
+
76
+ Raises:
77
+ RuntimeError: If called from within a running event loop
78
+
79
+ Example:
80
+ >>> translator = connect("0x3d40...")
81
+ >>> result = translator.input("Translate 'hello' to Spanish")
82
+ >>> # Continue conversation
83
+ >>> result2 = translator.input("Now translate it to French")
84
+ """
85
+ try:
86
+ asyncio.get_running_loop()
87
+ raise RuntimeError(
88
+ "input() cannot be used inside async context (e.g., Jupyter notebooks). "
89
+ "Use 'await agent.input_async()' instead."
90
+ )
91
+ except RuntimeError as e:
92
+ if "input() cannot be used" in str(e):
93
+ raise
94
+ # No running loop - safe to proceed
95
+ return asyncio.run(self._send_task(prompt, timeout))
96
+
97
+ async def input_async(self, prompt: str, timeout: float = 30.0) -> str:
98
+ """
99
+ Send task to remote agent and get response (async version).
100
+
101
+ Automatically maintains conversation context across calls.
102
+
103
+ Args:
104
+ prompt: Task/prompt to send
105
+ timeout: Seconds to wait for response (default 30)
106
+
107
+ Returns:
108
+ Agent's response string
109
+ """
110
+ return await self._send_task(prompt, timeout)
111
+
112
+ def reset_conversation(self):
113
+ """Clear conversation history and start fresh."""
114
+ self._session = None
115
+
116
+ def _sign_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]:
117
+ """Sign a payload if keys are available."""
118
+ if not self._keys:
119
+ return {"prompt": payload.get("prompt", "")}
120
+
121
+ canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
122
+ signature = addr.sign(self._keys, canonical.encode())
123
+ return {
124
+ "payload": payload,
125
+ "from": self._keys["address"],
126
+ "signature": signature.hex()
127
+ }
128
+
129
+ async def _discover_endpoints(self) -> List[str]:
130
+ """Query relay API for agent endpoints."""
131
+ import aiohttp
132
+
133
+ # Convert wss://oo.openonion.ai/ws/announce to https://oo.openonion.ai
134
+ base_url = self._relay_url.replace("wss://", "https://").replace("ws://", "http://")
135
+ base_url = base_url.replace("/ws/announce", "")
136
+
137
+ async with aiohttp.ClientSession() as session:
138
+ async with session.get(f"{base_url}/api/relay/agents/{self.address}") as resp:
139
+ if resp.status == 200:
140
+ data = await resp.json()
141
+ if data.get("online"):
142
+ return data.get("endpoints", [])
143
+ return []
144
+
145
+ def _create_signed_body(self, prompt: str) -> Dict[str, Any]:
146
+ """Create signed request body for agent /input endpoint."""
147
+ payload = {"prompt": prompt, "to": self.address, "timestamp": int(time.time())}
148
+ body = self._sign_payload(payload)
149
+ if self._session:
150
+ body["session"] = self._session
151
+ return body
152
+
153
+ async def _send_http(self, endpoint: str, prompt: str, timeout: float) -> str:
154
+ """Send request via direct HTTP POST to agent /input endpoint."""
155
+ import aiohttp
156
+
157
+ body = self._create_signed_body(prompt)
158
+
159
+ async with aiohttp.ClientSession() as http_session:
160
+ async with http_session.post(
161
+ f"{endpoint}/input",
162
+ json=body,
163
+ timeout=aiohttp.ClientTimeout(total=timeout)
164
+ ) as resp:
165
+ data = await resp.json()
166
+ if not resp.ok:
167
+ raise ConnectionError(data.get("error", f"HTTP {resp.status}"))
168
+ # Save session for conversation continuation
169
+ if "session" in data:
170
+ self._session = data["session"]
171
+ return data.get("result", "")
172
+
173
+ async def _send_relay(self, prompt: str, timeout: float) -> str:
174
+ """Send request via WebSocket relay."""
175
+ import websockets
176
+
177
+ input_id = str(uuid.uuid4())
178
+ relay_input_url = self._relay_url.replace("/ws/announce", "/ws/input")
179
+
180
+ async with websockets.connect(relay_input_url) as ws:
181
+ payload = {"prompt": prompt, "to": self.address, "timestamp": int(time.time())}
182
+ signed = self._sign_payload(payload)
183
+
184
+ input_message = {
185
+ "type": "INPUT",
186
+ "input_id": input_id,
187
+ "to": self.address,
188
+ **signed
189
+ }
190
+
191
+ await ws.send(json.dumps(input_message))
192
+
193
+ response_data = await asyncio.wait_for(ws.recv(), timeout=timeout)
194
+ response = json.loads(response_data)
195
+
196
+ if response.get("type") == "OUTPUT" and response.get("input_id") == input_id:
197
+ return response.get("result", "")
198
+ elif response.get("type") == "ERROR":
199
+ raise ConnectionError(f"Agent error: {response.get('error')}")
200
+ else:
201
+ raise ConnectionError(f"Unexpected response: {response}")
202
+
203
+ async def _send_task(self, prompt: str, timeout: float) -> str:
204
+ """
205
+ Send task using best available connection method.
206
+
207
+ Priority:
208
+ 1. Cached endpoint (if previously successful)
209
+ 2. Discovered HTTPS endpoints
210
+ 3. Discovered HTTP endpoints
211
+ 4. Relay fallback
212
+ """
213
+ # Try cached endpoint first
214
+ if self._cached_endpoint:
215
+ try:
216
+ return await self._send_http(self._cached_endpoint, prompt, timeout)
217
+ except Exception:
218
+ self._cached_endpoint = None # Clear failed cache
219
+
220
+ # Discover endpoints
221
+ endpoints = await self._discover_endpoints()
222
+
223
+ # Sort: HTTPS first, then HTTP
224
+ endpoints.sort(key=lambda e: (0 if e.startswith("https://") else 1))
225
+
226
+ # Try each endpoint
227
+ for endpoint in endpoints:
228
+ try:
229
+ result = await self._send_http(endpoint, prompt, timeout)
230
+ self._cached_endpoint = endpoint # Cache successful endpoint
231
+ return result
232
+ except Exception:
233
+ continue
234
+
235
+ # Fallback to relay
236
+ return await self._send_relay(prompt, timeout)
237
+
238
+ def __repr__(self):
239
+ short = self.address[:12] + "..." if len(self.address) > 12 else self.address
240
+ return f"RemoteAgent({short})"
241
+
242
+
243
+ def connect(
244
+ address: str,
245
+ *,
246
+ keys: Optional[Dict[str, Any]] = None,
247
+ relay_url: str = "wss://oo.openonion.ai/ws/announce"
248
+ ) -> RemoteAgent:
249
+ """
250
+ Connect to a remote agent.
251
+
252
+ Args:
253
+ address: Agent's public key address (0x...)
254
+ keys: Signing keys from address.load() - required for strict trust agents
255
+ relay_url: Relay server URL (default: production)
256
+
257
+ Returns:
258
+ RemoteAgent interface
259
+
260
+ Example:
261
+ >>> from connectonion import connect, address
262
+ >>>
263
+ >>> # Simple (unsigned)
264
+ >>> agent = connect("0x3d4017c3...")
265
+ >>> result = agent.input("Hello")
266
+ >>>
267
+ >>> # With signing (for strict trust agents)
268
+ >>> keys = address.load(Path(".co"))
269
+ >>> agent = connect("0x3d4017c3...", keys=keys)
270
+ >>> result = agent.input("Hello")
271
+ """
272
+ return RemoteAgent(address, keys=keys, relay_url=relay_url)
@@ -9,7 +9,13 @@ Trust parameter accepts three forms:
9
9
  3. Agent: Custom Agent instance for verification
10
10
 
11
11
  All forms create a trust agent behind the scenes.
12
+
13
+ Worker Isolation:
14
+ Each request gets a fresh deep copy of the agent template.
15
+ This ensures complete isolation - tools with state (like BrowserTool)
16
+ don't interfere between concurrent requests.
12
17
  """
18
+ import copy
13
19
  import hashlib
14
20
  import json
15
21
  import logging
@@ -102,17 +108,18 @@ class SessionStorage:
102
108
 
103
109
  # === Handlers (pure functions) ===
104
110
 
105
- def input_handler(agent, storage: SessionStorage, prompt: str, result_ttl: int,
111
+ def input_handler(agent_template, storage: SessionStorage, prompt: str, result_ttl: int,
106
112
  session: dict | None = None) -> dict:
107
113
  """POST /input
108
114
 
109
115
  Args:
110
- agent: The agent to process the request
116
+ agent_template: The agent template (deep copied per request for isolation)
111
117
  storage: SessionStorage for persisting results
112
118
  prompt: The user's prompt
113
119
  result_ttl: How long to keep the result on server
114
120
  session: Optional conversation session for continuation
115
121
  """
122
+ agent = copy.deepcopy(agent_template)
116
123
  now = time.time()
117
124
 
118
125
  # Get or generate session_id
@@ -368,7 +375,12 @@ def admin_logs_handler(agent_name: str) -> dict:
368
375
 
369
376
 
370
377
  def admin_sessions_handler() -> dict:
371
- """GET /admin/sessions - return all activity sessions as JSON array."""
378
+ """GET /admin/sessions - return raw session YAML files as JSON.
379
+
380
+ Returns session files as-is (converted from YAML to JSON). Each session
381
+ contains: name, created, updated, total_cost, total_tokens, turns array.
382
+ Frontend handles the display logic.
383
+ """
372
384
  import yaml
373
385
  sessions_dir = Path(".co/sessions")
374
386
  if not sessions_dir.exists():
@@ -381,30 +393,34 @@ def admin_sessions_handler() -> dict:
381
393
  if session_data:
382
394
  sessions.append(session_data)
383
395
 
384
- # Sort by created date descending (newest first)
385
- sessions.sort(key=lambda s: s.get("created", ""), reverse=True)
396
+ # Sort by updated date descending (newest first)
397
+ sessions.sort(key=lambda s: s.get("updated", s.get("created", "")), reverse=True)
386
398
  return {"sessions": sessions}
387
399
 
388
400
 
389
401
  # === Entry Point ===
390
402
 
391
- def _create_handlers(agent, result_ttl: int):
403
+ def _create_handlers(agent_template, result_ttl: int):
392
404
  """Create handler dict for ASGI app."""
405
+ def ws_input(prompt: str) -> str:
406
+ agent = copy.deepcopy(agent_template)
407
+ return agent.input(prompt)
408
+
393
409
  return {
394
- "input": lambda storage, prompt, ttl, session=None: input_handler(agent, storage, prompt, ttl, session),
410
+ "input": lambda storage, prompt, ttl, session=None: input_handler(agent_template, storage, prompt, ttl, session),
395
411
  "session": session_handler,
396
412
  "sessions": sessions_handler,
397
- "health": lambda start_time: health_handler(agent, start_time),
398
- "info": lambda trust: info_handler(agent, trust),
413
+ "health": lambda start_time: health_handler(agent_template, start_time),
414
+ "info": lambda trust: info_handler(agent_template, trust),
399
415
  "auth": extract_and_authenticate,
400
- "ws_input": agent.input,
416
+ "ws_input": ws_input,
401
417
  # Admin endpoints (auth required via OPENONION_API_KEY)
402
- "admin_logs": lambda: admin_logs_handler(agent.name),
418
+ "admin_logs": lambda: admin_logs_handler(agent_template.name),
403
419
  "admin_sessions": admin_sessions_handler,
404
420
  }
405
421
 
406
422
 
407
- def _start_relay_background(agent, relay_url: str, addr_data: dict):
423
+ def _start_relay_background(agent_template, relay_url: str, addr_data: dict):
408
424
  """Start relay connection in background thread.
409
425
 
410
426
  The relay connection runs alongside the HTTP server, allowing the agent
@@ -415,11 +431,12 @@ def _start_relay_background(agent, relay_url: str, addr_data: dict):
415
431
  from . import announce, relay
416
432
 
417
433
  # Create ANNOUNCE message
418
- summary = agent.system_prompt[:1000] if agent.system_prompt else f"{agent.name} agent"
434
+ summary = agent_template.system_prompt[:1000] if agent_template.system_prompt else f"{agent_template.name} agent"
419
435
  announce_msg = announce.create_announce_message(addr_data, summary, endpoints=[])
420
436
 
421
- # Task handler that routes to agent.input()
437
+ # Task handler - deep copy for each request
422
438
  async def task_handler(prompt: str) -> str:
439
+ agent = copy.deepcopy(agent_template)
423
440
  return agent.input(prompt)
424
441
 
425
442
  async def relay_loop():