connectonion 0.5.10__py3-none-any.whl → 0.6.1__py3-none-any.whl

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 (65) hide show
  1. connectonion/__init__.py +17 -16
  2. connectonion/cli/browser_agent/browser.py +488 -145
  3. connectonion/cli/browser_agent/scroll_strategies.py +276 -0
  4. connectonion/cli/commands/copy_commands.py +24 -1
  5. connectonion/cli/commands/deploy_commands.py +15 -0
  6. connectonion/cli/commands/eval_commands.py +286 -0
  7. connectonion/cli/commands/project_cmd_lib.py +1 -1
  8. connectonion/cli/main.py +11 -0
  9. connectonion/console.py +5 -5
  10. connectonion/core/__init__.py +53 -0
  11. connectonion/{agent.py → core/agent.py} +18 -15
  12. connectonion/{llm.py → core/llm.py} +9 -19
  13. connectonion/{tool_executor.py → core/tool_executor.py} +3 -2
  14. connectonion/{tool_factory.py → core/tool_factory.py} +3 -1
  15. connectonion/debug/__init__.py +51 -0
  16. connectonion/{interactive_debugger.py → debug/auto_debug.py} +7 -7
  17. connectonion/{auto_debug_exception.py → debug/auto_debug_exception.py} +3 -3
  18. connectonion/{debugger_ui.py → debug/auto_debug_ui.py} +1 -1
  19. connectonion/{debug_explainer → debug/debug_explainer}/explain_agent.py +1 -1
  20. connectonion/{debug_explainer → debug/debug_explainer}/explain_context.py +1 -1
  21. connectonion/{execution_analyzer → debug/execution_analyzer}/execution_analysis.py +1 -1
  22. connectonion/debug/runtime_inspector/__init__.py +13 -0
  23. connectonion/{debug_agent → debug/runtime_inspector}/agent.py +1 -1
  24. connectonion/{xray.py → debug/xray.py} +1 -1
  25. connectonion/llm_do.py +1 -1
  26. connectonion/logger.py +305 -135
  27. connectonion/network/__init__.py +37 -0
  28. connectonion/{announce.py → network/announce.py} +1 -1
  29. connectonion/{asgi.py → network/asgi.py} +122 -2
  30. connectonion/{connect.py → network/connect.py} +1 -1
  31. connectonion/network/connection.py +123 -0
  32. connectonion/{host.py → network/host.py} +31 -11
  33. connectonion/{trust.py → network/trust.py} +1 -1
  34. connectonion/tui/__init__.py +22 -0
  35. connectonion/tui/chat.py +647 -0
  36. connectonion/useful_events_handlers/reflect.py +2 -2
  37. connectonion/useful_plugins/__init__.py +4 -3
  38. connectonion/useful_plugins/calendar_plugin.py +2 -2
  39. connectonion/useful_plugins/eval.py +2 -2
  40. connectonion/useful_plugins/gmail_plugin.py +2 -2
  41. connectonion/useful_plugins/image_result_formatter.py +2 -2
  42. connectonion/useful_plugins/re_act.py +2 -2
  43. connectonion/useful_plugins/shell_approval.py +2 -2
  44. connectonion/useful_plugins/ui_stream.py +164 -0
  45. {connectonion-0.5.10.dist-info → connectonion-0.6.1.dist-info}/METADATA +4 -3
  46. connectonion-0.6.1.dist-info/RECORD +123 -0
  47. connectonion/debug_agent/__init__.py +0 -13
  48. connectonion-0.5.10.dist-info/RECORD +0 -115
  49. /connectonion/{events.py → core/events.py} +0 -0
  50. /connectonion/{tool_registry.py → core/tool_registry.py} +0 -0
  51. /connectonion/{usage.py → core/usage.py} +0 -0
  52. /connectonion/{debug_explainer → debug/debug_explainer}/__init__.py +0 -0
  53. /connectonion/{debug_explainer → debug/debug_explainer}/explainer_prompt.md +0 -0
  54. /connectonion/{debug_explainer → debug/debug_explainer}/root_cause_analysis_prompt.md +0 -0
  55. /connectonion/{decorators.py → debug/decorators.py} +0 -0
  56. /connectonion/{execution_analyzer → debug/execution_analyzer}/__init__.py +0 -0
  57. /connectonion/{execution_analyzer → debug/execution_analyzer}/execution_analysis_prompt.md +0 -0
  58. /connectonion/{debug_agent → debug/runtime_inspector}/prompts/debug_assistant.md +0 -0
  59. /connectonion/{debug_agent → debug/runtime_inspector}/runtime_inspector.py +0 -0
  60. /connectonion/{relay.py → network/relay.py} +0 -0
  61. /connectonion/{static → network/static}/docs.html +0 -0
  62. /connectonion/{trust_agents.py → network/trust_agents.py} +0 -0
  63. /connectonion/{trust_functions.py → network/trust_functions.py} +0 -0
  64. {connectonion-0.5.10.dist-info → connectonion-0.6.1.dist-info}/WHEEL +0 -0
  65. {connectonion-0.5.10.dist-info → connectonion-0.6.1.dist-info}/entry_points.txt +0 -0
@@ -6,13 +6,48 @@ requests. Separated from host.py for better testing and smaller file size.
6
6
  Design decision: Raw ASGI instead of Starlette/FastAPI for full protocol control.
7
7
  See: docs/design-decisions/022-raw-asgi-implementation.md
8
8
  """
9
+ import asyncio
9
10
  import hmac
10
11
  import json
11
12
  import os
13
+ import queue
14
+ import threading
12
15
  from pathlib import Path
16
+ from typing import Any, Dict
13
17
 
14
18
  from pydantic import BaseModel
15
19
 
20
+ from .connection import Connection
21
+
22
+
23
+ class AsyncToSyncConnection(Connection):
24
+ """Bridge async WebSocket to sync Connection interface.
25
+
26
+ Uses queues to communicate between async WebSocket handler and sync agent code.
27
+ The agent runs in a thread, sending/receiving via queues.
28
+ The async handler pumps messages between WebSocket and queues.
29
+ """
30
+
31
+ def __init__(self):
32
+ self._outgoing: queue.Queue[Dict[str, Any]] = queue.Queue()
33
+ self._incoming: queue.Queue[Dict[str, Any]] = queue.Queue()
34
+ self._closed = False
35
+
36
+ def send(self, event: Dict[str, Any]) -> None:
37
+ """Queue event to be sent to client."""
38
+ if not self._closed:
39
+ self._outgoing.put(event)
40
+
41
+ def receive(self) -> Dict[str, Any]:
42
+ """Block until response from client."""
43
+ return self._incoming.get()
44
+
45
+ def close(self):
46
+ """Mark connection as closed."""
47
+ self._closed = True
48
+ # Unblock any waiting receive
49
+ self._incoming.put({"type": "connection_closed"})
50
+
16
51
 
17
52
  def _json_default(obj):
18
53
  """Handle non-serializable objects like Pydantic models.
@@ -190,6 +225,11 @@ async def handle_websocket(
190
225
  ):
191
226
  """Handle WebSocket connections at /ws.
192
227
 
228
+ Supports bidirectional communication via Connection interface:
229
+ - Agent sends events via connection.log() / connection.send()
230
+ - Agent requests approval via connection.request_approval()
231
+ - Client responds to approval requests
232
+
193
233
  Args:
194
234
  scope: ASGI scope dict
195
235
  receive: ASGI receive callable
@@ -229,9 +269,89 @@ async def handle_websocket(
229
269
  await send({"type": "websocket.send",
230
270
  "text": json.dumps({"type": "ERROR", "message": "prompt required"})})
231
271
  continue
232
- result = handlers["ws_input"](prompt)
272
+
273
+ # Create connection for bidirectional communication
274
+ connection = AsyncToSyncConnection()
275
+ agent_done = threading.Event()
276
+ result_holder = [None]
277
+
278
+ def run_agent():
279
+ result_holder[0] = handlers["ws_input"](prompt, connection)
280
+ agent_done.set()
281
+
282
+ # Start agent in thread
283
+ agent_thread = threading.Thread(target=run_agent, daemon=True)
284
+ agent_thread.start()
285
+
286
+ # Pump messages between WebSocket and connection
287
+ await _pump_messages(receive, send, connection, agent_done)
288
+
289
+ # Send final result
233
290
  await send({"type": "websocket.send",
234
- "text": json.dumps({"type": "OUTPUT", "result": result})})
291
+ "text": json.dumps({"type": "OUTPUT", "result": result_holder[0]})})
292
+
293
+
294
+ async def _pump_messages(ws_receive, ws_send, connection: AsyncToSyncConnection, agent_done: threading.Event):
295
+ """Pump messages between WebSocket and connection queues.
296
+
297
+ Runs until agent completes. Handles:
298
+ - Outgoing: connection._outgoing queue → WebSocket
299
+ - Incoming: WebSocket → connection._incoming queue (for approval responses)
300
+ """
301
+ loop = asyncio.get_event_loop()
302
+
303
+ async def send_outgoing():
304
+ """Send outgoing messages from connection to WebSocket."""
305
+ while not agent_done.is_set():
306
+ # Use run_in_executor for blocking queue.get
307
+ try:
308
+ event = await loop.run_in_executor(
309
+ None, lambda: connection._outgoing.get(timeout=0.05)
310
+ )
311
+ await ws_send({"type": "websocket.send", "text": json.dumps(event)})
312
+ except queue.Empty:
313
+ pass
314
+
315
+ # Drain remaining
316
+ while True:
317
+ try:
318
+ event = connection._outgoing.get_nowait()
319
+ await ws_send({"type": "websocket.send", "text": json.dumps(event)})
320
+ except queue.Empty:
321
+ break
322
+
323
+ async def receive_incoming():
324
+ """Receive incoming messages from WebSocket to connection."""
325
+ while not agent_done.is_set():
326
+ try:
327
+ msg = await asyncio.wait_for(ws_receive(), timeout=0.1)
328
+ if msg["type"] == "websocket.receive":
329
+ try:
330
+ data = json.loads(msg.get("text", "{}"))
331
+ connection._incoming.put(data)
332
+ except json.JSONDecodeError:
333
+ pass
334
+ elif msg["type"] == "websocket.disconnect":
335
+ connection.close()
336
+ break
337
+ except asyncio.TimeoutError:
338
+ continue
339
+
340
+ # Run both tasks concurrently
341
+ send_task = asyncio.create_task(send_outgoing())
342
+ recv_task = asyncio.create_task(receive_incoming())
343
+
344
+ # Wait for agent to complete
345
+ while not agent_done.is_set():
346
+ await asyncio.sleep(0.05)
347
+
348
+ # Cancel receive task and wait for send to finish draining
349
+ recv_task.cancel()
350
+ try:
351
+ await recv_task
352
+ except asyncio.CancelledError:
353
+ pass
354
+ await send_task
235
355
 
236
356
 
237
357
  def create_app(
@@ -19,7 +19,7 @@ import time
19
19
  import uuid
20
20
  from typing import Any, Dict, List, Optional
21
21
 
22
- from . import address as addr
22
+ from .. import address as addr
23
23
 
24
24
 
25
25
  class RemoteAgent:
@@ -0,0 +1,123 @@
1
+ """
2
+ Purpose: Connection interface for agent-client communication during hosted execution
3
+ LLM-Note:
4
+ Dependencies: imports from [abc, typing] | imported by [asgi.py, __init__.py] | tested by [tests/unit/test_connection.py]
5
+ Data flow: receives from host/asgi → WebSocket send/receive → provides log() and request_approval() to agent event handlers
6
+ State/Effects: stateless base class | WebSocketConnection wraps ASGI WebSocket for bidirectional messaging
7
+ Integration: exposes Connection (base), WebSocketConnection (ASGI adapter) | agent.connection set by host() during WebSocket requests
8
+ Performance: log() is fire-and-forget (non-blocking) | request_approval() blocks waiting for client response
9
+ Errors: WebSocketConnection raises if WebSocket closed unexpectedly
10
+ """
11
+
12
+ from abc import ABC, abstractmethod
13
+ from typing import Any, Dict
14
+
15
+
16
+ class Connection(ABC):
17
+ """Base connection interface for agent-client communication.
18
+
19
+ Two-layer API:
20
+ - Low-level: send(event), receive() - primitives for any communication
21
+ - High-level: log(type, **data), request_approval(tool, args) - common patterns
22
+
23
+ Usage in event handlers:
24
+ @after_llm
25
+ def on_thinking(agent):
26
+ if agent.connection:
27
+ agent.connection.log("thinking")
28
+
29
+ @before_each_tool
30
+ def on_tool(agent):
31
+ if agent.connection:
32
+ tool = agent.current_session['pending_tool']
33
+ if tool['name'] in DANGEROUS:
34
+ if not agent.connection.request_approval(tool['name'], tool['arguments']):
35
+ raise ToolRejected()
36
+ """
37
+
38
+ # ═══════════════════════════════════════════════════════
39
+ # LOW-LEVEL API (Primitives)
40
+ # ═══════════════════════════════════════════════════════
41
+
42
+ @abstractmethod
43
+ def send(self, event: Dict[str, Any]) -> None:
44
+ """Send any event to client.
45
+
46
+ Args:
47
+ event: Dict with at least 'type' key, e.g. {"type": "thinking"}
48
+ """
49
+ pass
50
+
51
+ @abstractmethod
52
+ def receive(self) -> Dict[str, Any]:
53
+ """Receive response from client.
54
+
55
+ Returns:
56
+ Dict response from client
57
+ """
58
+ pass
59
+
60
+ # ═══════════════════════════════════════════════════════
61
+ # HIGH-LEVEL API (Patterns)
62
+ # ═══════════════════════════════════════════════════════
63
+
64
+ def log(self, event_type: str, **data) -> None:
65
+ """One-way notification to client.
66
+
67
+ Common event types: thinking, tool_call, tool_result, complete, error
68
+
69
+ Args:
70
+ event_type: Type of event (e.g. "thinking", "tool_call")
71
+ **data: Additional data for the event
72
+
73
+ Example:
74
+ connection.log("thinking")
75
+ connection.log("tool_call", name="search", arguments={"q": "python"})
76
+ """
77
+ self.send({"type": event_type, **data})
78
+
79
+ def request_approval(self, tool: str, arguments: Dict[str, Any]) -> bool:
80
+ """Two-way: request permission, wait for response.
81
+
82
+ Sends approval_needed event and blocks until client responds.
83
+
84
+ Args:
85
+ tool: Name of tool requiring approval
86
+ arguments: Tool arguments to show user
87
+
88
+ Returns:
89
+ True if approved, False if rejected
90
+
91
+ Example:
92
+ if not connection.request_approval("delete_file", {"path": "/tmp/x"}):
93
+ raise ToolRejected()
94
+ """
95
+ self.send({"type": "approval_needed", "tool": tool, "arguments": arguments})
96
+ response = self.receive()
97
+ return response.get("approved", False)
98
+
99
+
100
+ class SyncWebSocketConnection(Connection):
101
+ """Synchronous WebSocket connection adapter.
102
+
103
+ Wraps async WebSocket send/receive for use in synchronous agent code.
104
+ Uses threading events to bridge async/sync boundary.
105
+ """
106
+
107
+ def __init__(self, send_callback, receive_callback):
108
+ """Initialize with send/receive callbacks.
109
+
110
+ Args:
111
+ send_callback: Callable that sends message to WebSocket
112
+ receive_callback: Callable that receives message from WebSocket
113
+ """
114
+ self._send = send_callback
115
+ self._receive = receive_callback
116
+
117
+ def send(self, event: Dict[str, Any]) -> None:
118
+ """Send event to client via WebSocket."""
119
+ self._send(event)
120
+
121
+ def receive(self) -> Dict[str, Any]:
122
+ """Receive response from client via WebSocket."""
123
+ return self._receive()
@@ -177,8 +177,8 @@ def health_handler(agent, start_time: float) -> dict:
177
177
 
178
178
  def info_handler(agent, trust: str) -> dict:
179
179
  """GET /info"""
180
- from . import __version__
181
- tools = agent.tools.list_names() if hasattr(agent.tools, "list_names") else []
180
+ from .. import __version__
181
+ tools = agent.tools.names() if hasattr(agent.tools, "names") else []
182
182
  return {
183
183
  "name": agent.name,
184
184
  "address": get_agent_address(agent),
@@ -338,7 +338,7 @@ def evaluate_with_trust_agent(trust_agent, prompt: str, identity: str, sig_valid
338
338
  (accepted, reason) tuple
339
339
  """
340
340
  from pydantic import BaseModel
341
- from .llm_do import llm_do
341
+ from ..llm_do import llm_do
342
342
 
343
343
  class TrustDecision(BaseModel):
344
344
  accept: bool
@@ -382,7 +382,7 @@ def admin_sessions_handler() -> dict:
382
382
  Frontend handles the display logic.
383
383
  """
384
384
  import yaml
385
- sessions_dir = Path(".co/sessions")
385
+ sessions_dir = Path(".co/evals")
386
386
  if not sessions_dir.exists():
387
387
  return {"sessions": []}
388
388
 
@@ -401,10 +401,17 @@ def admin_sessions_handler() -> dict:
401
401
  # === Entry Point ===
402
402
 
403
403
  def _create_handlers(agent_template, result_ttl: int):
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)
404
+ """Create handler dict for ASGI app.
405
+
406
+ Args:
407
+ agent_template: Agent used as template (deep-copied per request for isolation)
408
+ result_ttl: How long to keep results on server in seconds
409
+ """
410
+ def ws_input(prompt: str, connection) -> str:
411
+ """WebSocket input with bidirectional connection support."""
412
+ agent_template.reset_conversation()
413
+ agent_template.connection = connection
414
+ return agent_template.input(prompt)
408
415
 
409
416
  return {
410
417
  "input": lambda storage, prompt, ttl, session=None: input_handler(agent_template, storage, prompt, ttl, session),
@@ -425,6 +432,11 @@ def _start_relay_background(agent_template, relay_url: str, addr_data: dict):
425
432
 
426
433
  The relay connection runs alongside the HTTP server, allowing the agent
427
434
  to be discovered via P2P network while also serving HTTP requests.
435
+
436
+ Args:
437
+ agent_template: Agent used as template (deep-copied per request for isolation)
438
+ relay_url: WebSocket URL for P2P relay
439
+ addr_data: Agent address data (public key, address)
428
440
  """
429
441
  import asyncio
430
442
  import threading
@@ -466,8 +478,12 @@ def host(
466
478
  """
467
479
  Host an agent over HTTP/WebSocket with optional P2P relay discovery.
468
480
 
481
+ The agent is used as a template - each request gets a fresh deep copy
482
+ for complete isolation. This ensures tools with state (like BrowserTool)
483
+ don't interfere between concurrent requests.
484
+
469
485
  Args:
470
- agent: Agent to host
486
+ agent: Agent template (deep-copied per request for isolation)
471
487
  port: HTTP port (default: PORT env var or 8000)
472
488
  trust: Trust level, policy, or Agent:
473
489
  - Level: "open", "careful", "strict"
@@ -492,7 +508,7 @@ def host(
492
508
  GET /logs/sessions - Activity sessions (requires OPENONION_API_KEY)
493
509
  """
494
510
  import uvicorn
495
- from . import address
511
+ from .. import address
496
512
 
497
513
  # Use PORT env var if port not specified (for container deployments)
498
514
  if port is None:
@@ -543,8 +559,11 @@ def host(
543
559
  def _make_app(agent, trust: Union[str, "Agent"] = "careful", result_ttl=86400, *, blacklist=None, whitelist=None):
544
560
  """Create ASGI app for external uvicorn/gunicorn usage.
545
561
 
562
+ The agent is used as a template - each request gets a fresh deep copy
563
+ for complete isolation.
564
+
546
565
  Args:
547
- agent: Agent to host
566
+ agent: Agent template (deep-copied per request for isolation)
548
567
  trust: Trust level, policy, or Agent
549
568
  result_ttl: How long to keep results on server in seconds
550
569
  blacklist: Blocked identities
@@ -579,6 +598,7 @@ host.app = _make_app
579
598
  def create_app_compat(agent, storage, trust="careful", result_ttl=86400, *, blacklist=None, whitelist=None):
580
599
  """Create ASGI app (backward-compatible wrapper).
581
600
 
601
+ The agent is used as a template (deep-copied per request for isolation).
582
602
  Prefer using host.app(agent) for new code.
583
603
  """
584
604
  handlers = _create_handlers(agent, result_ttl)
@@ -59,7 +59,7 @@ def create_trust_agent(trust: Union[str, Path, 'Agent', None], api_key: Optional
59
59
  ValueError: If trust level is invalid
60
60
  FileNotFoundError: If trust policy file doesn't exist
61
61
  """
62
- from .agent import Agent # Import here to avoid circular dependency
62
+ from ..core.agent import Agent # Import here to avoid circular dependency
63
63
 
64
64
  # If None, check for environment default
65
65
  if trust is None:
@@ -37,8 +37,20 @@ from .status_bar import StatusBar, SimpleStatusBar, ProgressSegment
37
37
  from .divider import Divider
38
38
  from .pick import pick
39
39
  from .footer import Footer
40
+ from .chat import (
41
+ Chat,
42
+ TriggerAutoComplete,
43
+ ChatStatusBar,
44
+ HintsFooter,
45
+ WelcomeMessage,
46
+ UserMessage,
47
+ AssistantMessage,
48
+ ThinkingIndicator,
49
+ )
50
+ from textual_autocomplete import DropdownItem as CommandItem
40
51
 
41
52
  __all__ = [
53
+ # Rich-based TUI (legacy)
42
54
  "Input",
43
55
  "Dropdown",
44
56
  "DropdownItem",
@@ -54,4 +66,14 @@ __all__ = [
54
66
  "Divider",
55
67
  "pick",
56
68
  "Footer",
69
+ # Textual-based Chat
70
+ "Chat",
71
+ "TriggerAutoComplete",
72
+ "CommandItem",
73
+ "ChatStatusBar",
74
+ "HintsFooter",
75
+ "WelcomeMessage",
76
+ "UserMessage",
77
+ "AssistantMessage",
78
+ "ThinkingIndicator",
57
79
  ]