synapse-cli-agent 0.1.13__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.
- synapse/__init__.py +13 -0
- synapse/__main__.py +6 -0
- synapse/app/__init__.py +1 -0
- synapse/app/agent.py +492 -0
- synapse/app/agent_md.py +107 -0
- synapse/cli.py +750 -0
- synapse/commands/__init__.py +1 -0
- synapse/commands/compression.py +573 -0
- synapse/commands/helpers.py +22 -0
- synapse/commands/mcp.py +406 -0
- synapse/commands/model.py +173 -0
- synapse/commands/result.py +34 -0
- synapse/commands/sessions.py +443 -0
- synapse/commands/slash_cmds.py +521 -0
- synapse/commands/slash_complete.py +816 -0
- synapse/commands/theme.py +99 -0
- synapse/config.py +27 -0
- synapse/content/__init__.py +1 -0
- synapse/content/input_history.py +122 -0
- synapse/content/multimodal.py +733 -0
- synapse/content/prompts.py +249 -0
- synapse/content/skills_catalog.py +128 -0
- synapse/integrations/__init__.py +1 -0
- synapse/integrations/checkpoint_seed.py +281 -0
- synapse/integrations/codex_history.py +375 -0
- synapse/integrations/codex_import.py +393 -0
- synapse/integrations/codex_sessions.py +629 -0
- synapse/integrations/describe_image.py +370 -0
- synapse/integrations/http_clients.py +199 -0
- synapse/integrations/llm_openai_compat.py +90 -0
- synapse/integrations/llm_openai_websocket.py +187 -0
- synapse/integrations/mcp_client.py +646 -0
- synapse/integrations/vision_middleware.py +62 -0
- synapse/models/__init__.py +5 -0
- synapse/models/config.py +240 -0
- synapse/models/helpers.py +206 -0
- synapse/models/profile.py +59 -0
- synapse/models/registry.py +722 -0
- synapse/models_registry.py +7 -0
- synapse/observability/__init__.py +1 -0
- synapse/observability/startup_trace.py +127 -0
- synapse/runtime/__init__.py +1 -0
- synapse/runtime/async_runtime.py +176 -0
- synapse/runtime/backends.py +458 -0
- synapse/runtime/context_compact.py +249 -0
- synapse/runtime/execute_capture.py +48 -0
- synapse/runtime/fs_permissions.py +79 -0
- synapse/runtime/harness.py +57 -0
- synapse/runtime/hitl.py +197 -0
- synapse/runtime/interaction_ledger.py +82 -0
- synapse/runtime/middleware.py +802 -0
- synapse/runtime/model_request_compression_middleware.py +745 -0
- synapse/runtime/pathing.py +146 -0
- synapse/runtime/safety.py +184 -0
- synapse/runtime/steer.py +240 -0
- synapse/runtime/subagents.py +207 -0
- synapse/runtime/tool_ignore.py +221 -0
- synapse/runtime/tool_output_eval.py +118 -0
- synapse/runtime/tool_output_middleware.py +585 -0
- synapse/runtime/tool_output_usage_middleware.py +60 -0
- synapse/sessions/__init__.py +31 -0
- synapse/sessions/cancel_repair.py +208 -0
- synapse/sessions/session_recap.py +174 -0
- synapse/sessions/store.py +695 -0
- synapse/sessions/transcript.py +754 -0
- synapse/settings/__init__.py +5 -0
- synapse/settings/config_paths.py +184 -0
- synapse/settings/schema.py +464 -0
- synapse/tool_output/__init__.py +59 -0
- synapse/tool_output/detection.py +170 -0
- synapse/tool_output/metrics.py +32 -0
- synapse/tool_output/models.py +173 -0
- synapse/tool_output/pipeline.py +330 -0
- synapse/tool_output/repository.py +721 -0
- synapse/tool_output/transformers.py +648 -0
- synapse/tools/__init__.py +5 -0
- synapse/tools/session_tools.py +204 -0
- synapse/ui/__init__.py +10 -0
- synapse/ui/bottombar/__init__.py +73 -0
- synapse/ui/bottombar/components/__init__.py +143 -0
- synapse/ui/bottombar/components/key_hints.py +30 -0
- synapse/ui/bottombar/components/mcp.py +64 -0
- synapse/ui/bottombar/components/mode.py +24 -0
- synapse/ui/bottombar/components/model.py +28 -0
- synapse/ui/bottombar/components/thread.py +29 -0
- synapse/ui/bottombar/context.py +36 -0
- synapse/ui/bottombar/core.py +74 -0
- synapse/ui/dialogs/__init__.py +25 -0
- synapse/ui/dialogs/base.py +362 -0
- synapse/ui/dialogs/codex_session_list.py +84 -0
- synapse/ui/dialogs/compression_diagnostics.py +210 -0
- synapse/ui/dialogs/git_explore.py +702 -0
- synapse/ui/dialogs/mcp_panel.py +407 -0
- synapse/ui/dialogs/model_picker.py +128 -0
- synapse/ui/dialogs/safety_panel.py +63 -0
- synapse/ui/dialogs/session_list.py +98 -0
- synapse/ui/dialogs/theme_designer.py +863 -0
- synapse/ui/dialogs/theme_picker.py +113 -0
- synapse/ui/git_explore/__init__.py +31 -0
- synapse/ui/git_explore/engine.py +82 -0
- synapse/ui/git_explore/provider.py +242 -0
- synapse/ui/git_explore/unified.py +85 -0
- synapse/ui/rendering.py +350 -0
- synapse/ui/sink.py +70 -0
- synapse/ui/steer_widget.py +367 -0
- synapse/ui/stream.py +1207 -0
- synapse/ui/stream_events.py +421 -0
- synapse/ui/stream_runtime.py +252 -0
- synapse/ui/theme.py +1154 -0
- synapse/ui/timeline.py +621 -0
- synapse/ui/topbar/__init__.py +97 -0
- synapse/ui/topbar/components/__init__.py +150 -0
- synapse/ui/topbar/components/branch.py +41 -0
- synapse/ui/topbar/components/title.py +24 -0
- synapse/ui/topbar/components/tool_output.py +24 -0
- synapse/ui/topbar/components/usage.py +24 -0
- synapse/ui/topbar/components/workspace.py +32 -0
- synapse/ui/topbar/context.py +32 -0
- synapse/ui/topbar/core.py +979 -0
- synapse/ui/topbar/git_changes_popover.py +178 -0
- synapse/ui/topbar/git_chrome.py +475 -0
- synapse/ui/topbar/tool_output_popover.py +84 -0
- synapse/ui/topbar/widget.py +474 -0
- synapse/ui/tui.py +5717 -0
- synapse/ui/turn_rail.py +71 -0
- synapse/ui/user_turn.py +83 -0
- synapse/ui/welcome.py +261 -0
- synapse_cli_agent-0.1.13.dist-info/METADATA +412 -0
- synapse_cli_agent-0.1.13.dist-info/RECORD +131 -0
- synapse_cli_agent-0.1.13.dist-info/WHEEL +4 -0
- synapse_cli_agent-0.1.13.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Startup diagnostics and observability helpers."""
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Optional startup timing tracer.
|
|
2
|
+
|
|
3
|
+
Enable with env ``AGENT_STARTUP_TRACE=1`` (or ``true`` / ``yes``).
|
|
4
|
+
Prints cumulative stage timings to stderr when the process finishes
|
|
5
|
+
startup-critical work, or on demand via :func:`dump`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _env_enabled() -> bool:
|
|
21
|
+
raw = (os.environ.get("AGENT_STARTUP_TRACE") or "").strip().casefold()
|
|
22
|
+
return raw in {"1", "true", "yes", "on"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class StartupTrace:
|
|
27
|
+
enabled: bool = field(default_factory=_env_enabled)
|
|
28
|
+
t0: float = field(default_factory=time.perf_counter)
|
|
29
|
+
# each mark: (name, ms_since_start, ms_step)
|
|
30
|
+
marks: list[tuple[str, float, float]] = field(default_factory=list)
|
|
31
|
+
_last: float = field(default_factory=time.perf_counter)
|
|
32
|
+
|
|
33
|
+
def reset(self) -> None:
|
|
34
|
+
self.t0 = time.perf_counter()
|
|
35
|
+
self._last = self.t0
|
|
36
|
+
self.marks.clear()
|
|
37
|
+
|
|
38
|
+
def mark(self, name: str) -> None:
|
|
39
|
+
if not self.enabled:
|
|
40
|
+
return
|
|
41
|
+
now = time.perf_counter()
|
|
42
|
+
since = (now - self.t0) * 1000
|
|
43
|
+
step = (now - self._last) * 1000
|
|
44
|
+
self._last = now
|
|
45
|
+
self.marks.append((name, since, step))
|
|
46
|
+
|
|
47
|
+
@contextmanager
|
|
48
|
+
def span(self, name: str) -> Iterator[None]:
|
|
49
|
+
if not self.enabled:
|
|
50
|
+
yield
|
|
51
|
+
return
|
|
52
|
+
t0 = time.perf_counter()
|
|
53
|
+
try:
|
|
54
|
+
yield
|
|
55
|
+
finally:
|
|
56
|
+
now = time.perf_counter()
|
|
57
|
+
since = (now - self.t0) * 1000
|
|
58
|
+
step = (now - t0) * 1000
|
|
59
|
+
self._last = now
|
|
60
|
+
self.marks.append((name, since, step))
|
|
61
|
+
|
|
62
|
+
def dump(self, *, header: str = "startup-trace", file=None) -> None:
|
|
63
|
+
if not self.enabled or not self.marks:
|
|
64
|
+
return
|
|
65
|
+
out = file or sys.stderr
|
|
66
|
+
total = (time.perf_counter() - self.t0) * 1000
|
|
67
|
+
print(f"[{header}] total={total:.1f}ms stages={len(self.marks)}", file=out)
|
|
68
|
+
for name, since, step in self.marks:
|
|
69
|
+
print(f" +{step:8.1f}ms @{since:8.1f}ms {name}", file=out)
|
|
70
|
+
# Top offenders by step cost
|
|
71
|
+
top = sorted(self.marks, key=lambda x: x[2], reverse=True)[:8]
|
|
72
|
+
print(f"[{header}] top stages:", file=out)
|
|
73
|
+
for name, _since, step in top:
|
|
74
|
+
print(f" {step:8.1f}ms {name}", file=out)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
TRACE = StartupTrace()
|
|
78
|
+
_LOCAL = threading.local()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _current_trace() -> StartupTrace:
|
|
82
|
+
if threading.current_thread() is threading.main_thread():
|
|
83
|
+
return TRACE
|
|
84
|
+
trace = getattr(_LOCAL, "trace", None)
|
|
85
|
+
if trace is None:
|
|
86
|
+
trace = StartupTrace()
|
|
87
|
+
_LOCAL.trace = trace
|
|
88
|
+
return trace
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def reset() -> None:
|
|
92
|
+
_current_trace().reset()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def mark(name: str) -> None:
|
|
96
|
+
_current_trace().mark(name)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def span(name: str):
|
|
100
|
+
return _current_trace().span(name)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def dump(**kwargs) -> None:
|
|
104
|
+
_current_trace().dump(**kwargs)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def duration(name: str, started: float, *, file=None, **fields: Any) -> None:
|
|
108
|
+
"""Emit one independent duration line without mutating cumulative stage state."""
|
|
109
|
+
if not _current_trace().enabled:
|
|
110
|
+
return
|
|
111
|
+
elapsed = (time.perf_counter() - started) * 1000
|
|
112
|
+
suffix = " ".join(f"{key}={value}" for key, value in fields.items())
|
|
113
|
+
detail = f" {suffix}" if suffix else ""
|
|
114
|
+
out = file or sys.stderr
|
|
115
|
+
print(
|
|
116
|
+
f"[perf] thread={threading.current_thread().name} event={name} "
|
|
117
|
+
f"elapsed_ms={elapsed:.1f}{detail}",
|
|
118
|
+
file=out,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def ensure_started() -> None:
|
|
123
|
+
"""Call near process entry so t0 is close to CLI start when enabled late."""
|
|
124
|
+
trace = _current_trace()
|
|
125
|
+
if trace.enabled and not trace.marks:
|
|
126
|
+
trace.reset()
|
|
127
|
+
trace.mark("trace-enabled")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runtime execution, middleware, and safety services."""
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Process-lifetime asyncio loop for AsyncSqliteSaver + astream.
|
|
2
|
+
|
|
3
|
+
LangGraph's ``AsyncSqliteSaver`` binds to the event loop that created it.
|
|
4
|
+
TUI / CLI call sites are mostly synchronous and previously used
|
|
5
|
+
``asyncio.run`` in a worker thread (a *new* loop every turn) — that cannot
|
|
6
|
+
share an AsyncSqliteSaver.
|
|
7
|
+
|
|
8
|
+
This module owns one background loop thread for the whole process:
|
|
9
|
+
|
|
10
|
+
- open/setup AsyncSqliteSaver on that loop
|
|
11
|
+
- schedule ``agent.astream`` coroutines onto the same loop
|
|
12
|
+
- sync ``get_tuple`` (via LangGraph) still works from other threads via
|
|
13
|
+
``run_coroutine_threadsafe``
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import atexit
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from collections.abc import Coroutine
|
|
23
|
+
from typing import Any, TypeVar
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncRuntime:
|
|
29
|
+
"""Daemon thread running ``loop.run_forever()`` for checkpoint + astream."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, *, name: str = "coding-async-runtime") -> None:
|
|
32
|
+
self._name = name
|
|
33
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
34
|
+
self._thread: threading.Thread | None = None
|
|
35
|
+
self._ready = threading.Event()
|
|
36
|
+
self._lock = threading.Lock()
|
|
37
|
+
self._closed = False
|
|
38
|
+
self._owned_conns: list[Any] = []
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def loop(self) -> asyncio.AbstractEventLoop:
|
|
42
|
+
self.start()
|
|
43
|
+
assert self._loop is not None
|
|
44
|
+
return self._loop
|
|
45
|
+
|
|
46
|
+
def start(self) -> None:
|
|
47
|
+
with self._lock:
|
|
48
|
+
if self._loop is not None and self._thread is not None and self._thread.is_alive():
|
|
49
|
+
return
|
|
50
|
+
if self._closed:
|
|
51
|
+
raise RuntimeError("AsyncRuntime is closed")
|
|
52
|
+
self._ready.clear()
|
|
53
|
+
self._thread = threading.Thread(
|
|
54
|
+
target=self._thread_main,
|
|
55
|
+
name=self._name,
|
|
56
|
+
daemon=True,
|
|
57
|
+
)
|
|
58
|
+
self._thread.start()
|
|
59
|
+
if not self._ready.wait(timeout=5.0):
|
|
60
|
+
raise RuntimeError("AsyncRuntime loop failed to start")
|
|
61
|
+
|
|
62
|
+
def _thread_main(self) -> None:
|
|
63
|
+
loop = asyncio.new_event_loop()
|
|
64
|
+
asyncio.set_event_loop(loop)
|
|
65
|
+
self._loop = loop
|
|
66
|
+
self._ready.set()
|
|
67
|
+
try:
|
|
68
|
+
loop.run_forever()
|
|
69
|
+
finally:
|
|
70
|
+
try:
|
|
71
|
+
pending = asyncio.all_tasks(loop)
|
|
72
|
+
for task in pending:
|
|
73
|
+
task.cancel()
|
|
74
|
+
if pending:
|
|
75
|
+
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
|
76
|
+
except Exception: # noqa: BLE001
|
|
77
|
+
pass
|
|
78
|
+
loop.close()
|
|
79
|
+
|
|
80
|
+
def submit(self, coro: Coroutine[Any, Any, T]) -> asyncio.Future[T]:
|
|
81
|
+
"""Schedule a coroutine on the runtime loop (thread-safe)."""
|
|
82
|
+
return asyncio.run_coroutine_threadsafe(coro, self.loop)
|
|
83
|
+
|
|
84
|
+
def run(self, coro: Coroutine[Any, Any, T], *, timeout: float | None = None) -> T:
|
|
85
|
+
"""Block until the coroutine finishes on the runtime loop."""
|
|
86
|
+
return self.submit(coro).result(timeout=timeout)
|
|
87
|
+
|
|
88
|
+
def track_connection(self, conn: Any) -> None:
|
|
89
|
+
"""Remember an async connection/client for shutdown."""
|
|
90
|
+
if conn not in self._owned_conns:
|
|
91
|
+
self._owned_conns.append(conn)
|
|
92
|
+
|
|
93
|
+
def close_connection(self, conn: Any) -> None:
|
|
94
|
+
"""Close and forget one tracked async client on the runtime loop."""
|
|
95
|
+
try:
|
|
96
|
+
self._owned_conns.remove(conn)
|
|
97
|
+
except ValueError:
|
|
98
|
+
pass
|
|
99
|
+
close = getattr(conn, "aclose", None) or getattr(conn, "close", None)
|
|
100
|
+
if not callable(close):
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
async def _close() -> None:
|
|
104
|
+
result = close()
|
|
105
|
+
if asyncio.iscoroutine(result):
|
|
106
|
+
await result
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
self.run(_close(), timeout=3.0)
|
|
110
|
+
except Exception: # noqa: BLE001
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def close(self) -> None:
|
|
114
|
+
started = time.perf_counter()
|
|
115
|
+
with self._lock:
|
|
116
|
+
if self._closed:
|
|
117
|
+
return
|
|
118
|
+
self._closed = True
|
|
119
|
+
loop = self._loop
|
|
120
|
+
thread = self._thread
|
|
121
|
+
connection_count = len(self._owned_conns)
|
|
122
|
+
if loop is not None and loop.is_running():
|
|
123
|
+
|
|
124
|
+
async def _shutdown() -> None:
|
|
125
|
+
for conn in list(self._owned_conns):
|
|
126
|
+
try:
|
|
127
|
+
close = getattr(conn, "aclose", None) or getattr(conn, "close", None)
|
|
128
|
+
if not callable(close):
|
|
129
|
+
continue
|
|
130
|
+
result = close()
|
|
131
|
+
if asyncio.iscoroutine(result):
|
|
132
|
+
await result
|
|
133
|
+
except Exception: # noqa: BLE001
|
|
134
|
+
pass
|
|
135
|
+
self._owned_conns.clear()
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
asyncio.run_coroutine_threadsafe(_shutdown(), loop).result(timeout=3.0)
|
|
139
|
+
except Exception: # noqa: BLE001
|
|
140
|
+
pass
|
|
141
|
+
finally:
|
|
142
|
+
try:
|
|
143
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
144
|
+
except Exception: # noqa: BLE001
|
|
145
|
+
pass
|
|
146
|
+
if thread is not None and thread.is_alive():
|
|
147
|
+
thread.join(timeout=2.0)
|
|
148
|
+
self._loop = None
|
|
149
|
+
self._thread = None
|
|
150
|
+
try:
|
|
151
|
+
from synapse.observability.startup_trace import duration
|
|
152
|
+
|
|
153
|
+
duration("runtime.close", started, connections=connection_count)
|
|
154
|
+
except Exception: # noqa: BLE001
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
_RUNTIME = AsyncRuntime()
|
|
159
|
+
atexit.register(_RUNTIME.close)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_async_runtime() -> AsyncRuntime:
|
|
163
|
+
"""Process-global async runtime (lazy-started)."""
|
|
164
|
+
return _RUNTIME
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def reset_async_runtime_for_tests() -> AsyncRuntime:
|
|
168
|
+
"""Close and replace the global runtime (tests only)."""
|
|
169
|
+
global _RUNTIME
|
|
170
|
+
try:
|
|
171
|
+
_RUNTIME.close()
|
|
172
|
+
except Exception: # noqa: BLE001
|
|
173
|
+
pass
|
|
174
|
+
_RUNTIME = AsyncRuntime(name="coding-async-runtime-test")
|
|
175
|
+
atexit.register(_RUNTIME.close)
|
|
176
|
+
return _RUNTIME
|