ariadex 0.1.0__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.
- ariadex/__init__.py +3 -0
- ariadex/__main__.py +6 -0
- ariadex/adapters.py +190 -0
- ariadex/cli.py +1251 -0
- ariadex/concurrency.py +655 -0
- ariadex/config.py +270 -0
- ariadex/control.py +62 -0
- ariadex/handoff.py +342 -0
- ariadex/live_evidence.py +1252 -0
- ariadex/logging.py +465 -0
- ariadex/observability.py +637 -0
- ariadex/operator.py +700 -0
- ariadex/providers.py +86 -0
- ariadex/py.typed +0 -0
- ariadex/release.py +288 -0
- ariadex/resync.py +112 -0
- ariadex/runner.py +1012 -0
- ariadex/spec_graph.py +329 -0
- ariadex/state.py +114 -0
- ariadex/status.py +71 -0
- ariadex/terminal.py +255 -0
- ariadex/tmux_setup.py +342 -0
- ariadex/verify.py +176 -0
- ariadex-0.1.0.dist-info/METADATA +262 -0
- ariadex-0.1.0.dist-info/RECORD +29 -0
- ariadex-0.1.0.dist-info/WHEEL +5 -0
- ariadex-0.1.0.dist-info/entry_points.txt +2 -0
- ariadex-0.1.0.dist-info/licenses/LICENSE +21 -0
- ariadex-0.1.0.dist-info/top_level.txt +1 -0
ariadex/__init__.py
ADDED
ariadex/__main__.py
ADDED
ariadex/adapters.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Provider-neutral agent lifecycle contract and capability model.
|
|
2
|
+
|
|
3
|
+
`AgentAdapter` owns provider command construction and lifecycle mapping:
|
|
4
|
+
start, send, interrupt, new session, output capture, idle detection, and
|
|
5
|
+
terminate. It never embeds terminal transport details (those belong to
|
|
6
|
+
`TerminalDriver`) and never calls an LLM API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import abc
|
|
12
|
+
import dataclasses
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from . import terminal as terminal_mod
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclasses.dataclass(frozen=True)
|
|
19
|
+
class Capabilities:
|
|
20
|
+
"""Inspectable adapter capabilities consumed by the runner."""
|
|
21
|
+
|
|
22
|
+
interactive: bool = True
|
|
23
|
+
soft_reset: bool = False
|
|
24
|
+
hard_reset: bool = True
|
|
25
|
+
token_usage: bool = False
|
|
26
|
+
structured_output: bool = False
|
|
27
|
+
interrupt: bool = True
|
|
28
|
+
manual_takeover: bool = True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AdapterError(Exception):
|
|
32
|
+
"""Base class for typed adapter failures."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UnsupportedOperation(AdapterError):
|
|
36
|
+
"""The adapter explicitly does not support the requested operation."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StartupError(AdapterError):
|
|
40
|
+
"""Provider startup failed."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TransportError(AdapterError):
|
|
44
|
+
"""Input delivery to a running provider failed."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CaptureError(AdapterError):
|
|
48
|
+
"""Output capture from a running provider failed."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TerminationError(AdapterError):
|
|
52
|
+
"""Provider termination failed."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def select_reset(reset_mode: str, capabilities: Capabilities) -> str:
|
|
56
|
+
"""Choose `soft` or `hard` reset from declared capabilities.
|
|
57
|
+
|
|
58
|
+
`auto` selects the strongest supported operation, preferring soft reset
|
|
59
|
+
when available. Raises UnsupportedOperation when the requested strength
|
|
60
|
+
is not declared.
|
|
61
|
+
"""
|
|
62
|
+
if reset_mode == "soft":
|
|
63
|
+
if not capabilities.soft_reset:
|
|
64
|
+
raise UnsupportedOperation("adapter lacks `soft_reset` capability")
|
|
65
|
+
return "soft"
|
|
66
|
+
if reset_mode == "hard":
|
|
67
|
+
if not capabilities.hard_reset:
|
|
68
|
+
raise UnsupportedOperation("adapter lacks `hard_reset` capability")
|
|
69
|
+
return "hard"
|
|
70
|
+
if reset_mode == "auto":
|
|
71
|
+
if capabilities.soft_reset:
|
|
72
|
+
return "soft"
|
|
73
|
+
if capabilities.hard_reset:
|
|
74
|
+
return "hard"
|
|
75
|
+
raise UnsupportedOperation(
|
|
76
|
+
"adapter declares neither `soft_reset` nor `hard_reset`"
|
|
77
|
+
)
|
|
78
|
+
raise AdapterError(
|
|
79
|
+
f"invalid reset mode `{reset_mode}`: expected soft, hard, or auto"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AgentAdapter(abc.ABC):
|
|
84
|
+
"""One lifecycle contract implemented per Coding CLI provider."""
|
|
85
|
+
|
|
86
|
+
provider_name: str = "unknown"
|
|
87
|
+
launch_command: tuple[str, ...] = ()
|
|
88
|
+
new_session_input: str | None = None
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
driver: terminal_mod.TerminalDriver,
|
|
93
|
+
session_name: str,
|
|
94
|
+
workdir: Path | str,
|
|
95
|
+
) -> None:
|
|
96
|
+
self.driver = driver
|
|
97
|
+
self.session_name = session_name
|
|
98
|
+
self.workdir = workdir
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
@abc.abstractmethod
|
|
102
|
+
def capabilities(self) -> Capabilities:
|
|
103
|
+
"""Declared capabilities; the runner must consult these."""
|
|
104
|
+
|
|
105
|
+
def start(self) -> str:
|
|
106
|
+
"""Create or connect the session and launch the provider.
|
|
107
|
+
|
|
108
|
+
Returns `created` or `connected`. Wraps driver failures so callers
|
|
109
|
+
only handle AdapterError.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
return self.driver.create_or_connect(
|
|
113
|
+
self.session_name, self.workdir, list(self.launch_command)
|
|
114
|
+
)
|
|
115
|
+
except terminal_mod.TerminalError as exc:
|
|
116
|
+
raise StartupError(f"{self.provider_name} startup failed: {exc}") from exc
|
|
117
|
+
|
|
118
|
+
def send(self, text: str) -> None:
|
|
119
|
+
try:
|
|
120
|
+
self.driver.send_input(self.session_name, text)
|
|
121
|
+
except terminal_mod.TmuxNotAvailable as exc:
|
|
122
|
+
raise TransportError(str(exc)) from exc
|
|
123
|
+
except terminal_mod.SessionMissing as exc:
|
|
124
|
+
raise TransportError(str(exc)) from exc
|
|
125
|
+
except terminal_mod.TerminalError as exc:
|
|
126
|
+
raise TransportError(
|
|
127
|
+
f"failed to deliver input to {self.provider_name}: {exc}"
|
|
128
|
+
) from exc
|
|
129
|
+
|
|
130
|
+
def interrupt(self) -> None:
|
|
131
|
+
if not self.capabilities.interrupt:
|
|
132
|
+
raise UnsupportedOperation(
|
|
133
|
+
f"{self.provider_name} lacks `interrupt` capability"
|
|
134
|
+
)
|
|
135
|
+
try:
|
|
136
|
+
self.driver.interrupt(self.session_name)
|
|
137
|
+
except terminal_mod.TerminalError as exc:
|
|
138
|
+
raise TransportError(
|
|
139
|
+
f"failed to interrupt {self.provider_name}: {exc}"
|
|
140
|
+
) from exc
|
|
141
|
+
|
|
142
|
+
def new_session(self) -> None:
|
|
143
|
+
"""Soft reset via the provider's new-session input.
|
|
144
|
+
|
|
145
|
+
Reports the missing capability so the runner can fall back to hard
|
|
146
|
+
reset (terminate and restart) instead of assuming `/new` works.
|
|
147
|
+
"""
|
|
148
|
+
if not self.capabilities.soft_reset or self.new_session_input is None:
|
|
149
|
+
raise UnsupportedOperation(
|
|
150
|
+
f"{self.provider_name} lacks `soft_reset` capability; "
|
|
151
|
+
"use hard reset (terminate and restart)"
|
|
152
|
+
)
|
|
153
|
+
try:
|
|
154
|
+
self.driver.send_input(self.session_name, self.new_session_input)
|
|
155
|
+
except terminal_mod.TerminalError as exc:
|
|
156
|
+
raise TransportError(
|
|
157
|
+
f"failed to reset {self.provider_name} session: {exc}"
|
|
158
|
+
) from exc
|
|
159
|
+
|
|
160
|
+
def capture_output(self) -> str:
|
|
161
|
+
"""Return raw pane output verbatim for the logger.
|
|
162
|
+
|
|
163
|
+
Never interprets provider text as completion.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
return self.driver.capture(self.session_name)
|
|
167
|
+
except terminal_mod.TerminalError as exc:
|
|
168
|
+
raise CaptureError(
|
|
169
|
+
f"failed to capture {self.provider_name} output: {exc}"
|
|
170
|
+
) from exc
|
|
171
|
+
|
|
172
|
+
def is_idle(self) -> bool:
|
|
173
|
+
"""Conservative default: never claim idle without evidence."""
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def get_usage(self) -> dict | None:
|
|
177
|
+
"""Provider token/cost usage, or None when not exposed.
|
|
178
|
+
|
|
179
|
+
Metrics must record `usage: unavailable` in that case and never
|
|
180
|
+
invent an estimate.
|
|
181
|
+
"""
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
def terminate(self) -> None:
|
|
185
|
+
try:
|
|
186
|
+
self.driver.terminate(self.session_name)
|
|
187
|
+
except terminal_mod.TerminalError as exc:
|
|
188
|
+
raise TerminationError(
|
|
189
|
+
f"failed to terminate {self.provider_name}: {exc}"
|
|
190
|
+
) from exc
|