main
py 168 lines 5.12 KB
Raw
1 """Context event streaming bridge for the a0-connector plugin."""
2 from __future__ import annotations
3
4 import asyncio
5 import time
6 from typing import Any, AsyncIterator, Callable
7
8 from helpers.print_style import PrintStyle
9
10
11 EVENT_USER_MESSAGE = "user_message"
12 EVENT_ASSISTANT_DELTA = "assistant_delta"
13 EVENT_ASSISTANT_MESSAGE = "assistant_message"
14 EVENT_TOOL_START = "tool_start"
15 EVENT_TOOL_OUTPUT = "tool_output"
16 EVENT_TOOL_END = "tool_end"
17 EVENT_CODE_START = "code_start"
18 EVENT_CODE_OUTPUT = "code_output"
19 EVENT_WARNING = "warning"
20 EVENT_ERROR = "error"
21 EVENT_INFO = "info"
22 EVENT_STATUS = "status"
23 EVENT_UTIL_MESSAGE = "util_message"
24 EVENT_MESSAGE_COMPLETE = "message_complete"
25 EVENT_CONTEXT_UPDATED = "context_updated"
26
27 _LOG_TYPE_MAP: dict[str, str] = {
28 "agent": EVENT_STATUS,
29 "ai_response": EVENT_ASSISTANT_MESSAGE,
30 "browser": EVENT_TOOL_OUTPUT,
31 "code": EVENT_CODE_START,
32 "code_exe": EVENT_CODE_OUTPUT,
33 "code_output": EVENT_CODE_OUTPUT,
34 "error": EVENT_ERROR,
35 "hint": EVENT_STATUS,
36 "info": EVENT_INFO,
37 "input": EVENT_USER_MESSAGE,
38 "mcp": EVENT_TOOL_START,
39 "progress": EVENT_STATUS,
40 "response": EVENT_ASSISTANT_MESSAGE,
41 "subagent": EVENT_STATUS,
42 "tool": EVENT_TOOL_START,
43 "tool_output": EVENT_TOOL_OUTPUT,
44 "user": EVENT_USER_MESSAGE,
45 "util": EVENT_UTIL_MESSAGE,
46 "warning": EVENT_WARNING,
47 }
48
49
50 def log_entry_to_connector_event(
51 log_entry: dict[str, Any],
52 context_id: str,
53 ) -> dict[str, Any]:
54 entry_type = str(log_entry.get("type", "")).strip()
55 event_type = _LOG_TYPE_MAP.get(entry_type, EVENT_STATUS)
56 item_no = int(log_entry.get("no", 0) or 0)
57
58 data: dict[str, Any] = {}
59 content = log_entry.get("content")
60 heading = log_entry.get("heading")
61 kvps = log_entry.get("kvps")
62
63 if isinstance(content, str) and content:
64 data["text"] = content
65 if isinstance(heading, str) and heading:
66 data["heading"] = heading
67 if isinstance(kvps, dict) and kvps:
68 data["meta"] = kvps
69
70 return {
71 "context_id": context_id,
72 "sequence": item_no + 1,
73 "event": event_type,
74 "timestamp": log_entry.get("timestamp", ""),
75 "data": data,
76 }
77
78
79 def get_context_log_entries(
80 context_id: str,
81 after: int = 0,
82 limit: int | None = None,
83 ) -> tuple[list[dict[str, Any]], int]:
84 """Return connector events plus the next log cursor for the context."""
85 try:
86 from agent import AgentContext
87
88 context = AgentContext.get(context_id)
89 if context is None:
90 return [], 0
91
92 start = max(int(after or 0), 0)
93 end: int | None = None
94 if limit is not None:
95 limit = max(int(limit or 0), 0)
96 if limit > 0:
97 log_lock = getattr(context.log, "_lock", None)
98 log_updates = getattr(context.log, "updates", None)
99 if log_lock is not None and isinstance(log_updates, list):
100 with log_lock:
101 end = min(start + limit, len(log_updates))
102 else:
103 end = start + limit
104
105 log_output = context.log.output(start=start, end=end)
106 events = [
107 log_entry_to_connector_event(entry, context_id)
108 for entry in log_output.items
109 if isinstance(entry, dict)
110 ]
111 return events, int(log_output.end)
112 except Exception as exc:
113 PrintStyle.error(
114 f"[a0-connector] event_bridge error for context {context_id}: {exc}"
115 )
116 return [], max(int(after or 0), 0)
117
118
119 def get_context_log_entry_count(context_id: str) -> int:
120 """Return the current log-output cursor for a context."""
121 try:
122 from agent import AgentContext
123
124 context = AgentContext.get(context_id)
125 if context is None:
126 return 0
127
128 log = context.log
129 log_lock = getattr(log, "_lock", None)
130 updates = getattr(log, "updates", None)
131 if isinstance(updates, list):
132 if log_lock is not None:
133 with log_lock:
134 return len(updates)
135 return len(updates)
136
137 return int(log.output().end)
138 except Exception as exc:
139 PrintStyle.error(
140 f"[a0-connector] event_bridge cursor error for context {context_id}: {exc}"
141 )
142 return 0
143
144
145 async def stream_context_events(
146 context_id: str,
147 from_sequence: int = 0,
148 poll_interval: float = 0.5,
149 timeout: float = 300.0,
150 emit_fn: Callable[[dict[str, Any]], Any] | None = None,
151 ) -> AsyncIterator[dict[str, Any]]:
152 cursor = max(int(from_sequence or 0), 0)
153 deadline = time.monotonic() + timeout
154
155 while time.monotonic() < deadline:
156 events, next_cursor = get_context_log_entries(context_id, after=cursor)
157 for event in events:
158 if emit_fn is not None:
159 try:
160 result = emit_fn(event)
161 if asyncio.iscoroutine(result):
162 await result
163 except Exception as exc:
164 PrintStyle.error(f"[a0-connector] emit_fn error: {exc}")
165 yield event
166
167 cursor = max(cursor, next_cursor)
168 await asyncio.sleep(poll_interval)