aimax 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.
- aimax/__init__.py +121 -0
- aimax/_version.py +7 -0
- aimax/base.py +305 -0
- aimax/cli.py +757 -0
- aimax/config.py +262 -0
- aimax/services/__init__.py +9 -0
- aimax/services/admin_settings.py +139 -0
- aimax/services/agent_prompt_events.py +128 -0
- aimax/services/jsonl_watcher.py +305 -0
- aimax/services/mobius_jsonl.py +367 -0
- aimax/tmux_claude_code.py +1144 -0
- aimax/tmux_codex.py +1499 -0
- aimax/utils/__init__.py +5 -0
- aimax/utils/session_flags.py +242 -0
- aimax-0.1.0.dist-info/METADATA +377 -0
- aimax-0.1.0.dist-info/RECORD +33 -0
- aimax-0.1.0.dist-info/WHEEL +4 -0
- aimax-0.1.0.dist-info/entry_points.txt +3 -0
- aimax-0.1.0.dist-info/licenses/LICENSE +21 -0
- tmux_agents/__init__.py +4 -0
- tmux_agents/_version.py +3 -0
- tmux_agents/base.py +3 -0
- tmux_agents/cli.py +3 -0
- tmux_agents/config.py +3 -0
- tmux_agents/services/__init__.py +3 -0
- tmux_agents/services/admin_settings.py +3 -0
- tmux_agents/services/agent_prompt_events.py +3 -0
- tmux_agents/services/jsonl_watcher.py +3 -0
- tmux_agents/services/mobius_jsonl.py +3 -0
- tmux_agents/tmux_claude_code.py +3 -0
- tmux_agents/tmux_codex.py +3 -0
- tmux_agents/utils/__init__.py +3 -0
- tmux_agents/utils/session_flags.py +3 -0
aimax/__init__.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""aimax — drive Claude Code and Codex TUIs through tmux from Python.
|
|
2
|
+
|
|
3
|
+
Quick start
|
|
4
|
+
===========
|
|
5
|
+
|
|
6
|
+
.. code-block:: python
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import aimax
|
|
10
|
+
|
|
11
|
+
async def main():
|
|
12
|
+
backend = aimax.get("tmux-codex")
|
|
13
|
+
await backend.create_new_session({
|
|
14
|
+
"sessionId": "my-session",
|
|
15
|
+
"cwd": "/tmp/work",
|
|
16
|
+
"initialPrompt": "List files in this directory.",
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
asyncio.run(main())
|
|
20
|
+
|
|
21
|
+
Supported backends
|
|
22
|
+
------------------
|
|
23
|
+
|
|
24
|
+
* ``"tmux-claude-code"`` — wraps Anthropic's ``claude`` TUI
|
|
25
|
+
(:class:`aimax.tmux_claude_code.TmuxClaudeCodeBackend`).
|
|
26
|
+
* ``"tmux-codex"`` — wraps OpenAI's ``codex`` TUI
|
|
27
|
+
(:class:`aimax.tmux_codex.TmuxCodexBackend`).
|
|
28
|
+
|
|
29
|
+
Each backend exposes the same async surface:
|
|
30
|
+
|
|
31
|
+
* ``create_new_session(opts)``
|
|
32
|
+
* ``no_pause_current_and_queue_query_at_session(opts)``
|
|
33
|
+
* ``pause_current_and_resume_from_session(opts)``
|
|
34
|
+
* ``terminate_session(session_id)``
|
|
35
|
+
* ``is_alive(session_id)`` / ``is_working(session_id)`` /
|
|
36
|
+
``is_job_goal_accomplished(session_id)`` / ``is_failed(session_id)``
|
|
37
|
+
* ``list_sessions()`` / ``get_history(session_id)`` /
|
|
38
|
+
``get_agent_raw_thought_stream(session_id, listener, opts)``
|
|
39
|
+
|
|
40
|
+
Configuration
|
|
41
|
+
-------------
|
|
42
|
+
|
|
43
|
+
Filesystem paths and hub names are configurable through environment
|
|
44
|
+
variables (see :mod:`aimax.config`) or by installing a custom
|
|
45
|
+
:class:`~aimax.config.TmuxAgentsConfig` *before* the first
|
|
46
|
+
:func:`get` call.
|
|
47
|
+
|
|
48
|
+
CLI
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
The package ships with a ``aimax`` console script — run
|
|
52
|
+
``aimax --help`` for a tour, or see :mod:`aimax.cli`.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
from typing import Dict
|
|
58
|
+
|
|
59
|
+
from . import config
|
|
60
|
+
from ._version import __version__
|
|
61
|
+
from .base import AgentBackend
|
|
62
|
+
from .config import AimaxConfig, TmuxAgentsConfig, get_config, reset_config, set_config
|
|
63
|
+
|
|
64
|
+
# Recognised backend names. Adding a new backend = extend this tuple and
|
|
65
|
+
# the ``get`` factory below.
|
|
66
|
+
SUPPORTED_BACKENDS = ("tmux-claude-code", "tmux-codex")
|
|
67
|
+
|
|
68
|
+
_singletons: Dict[str, AgentBackend] = {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get(name: str) -> AgentBackend:
|
|
72
|
+
"""Return the singleton backend named ``name``.
|
|
73
|
+
|
|
74
|
+
First call instantiates the backend (which may do a one-shot
|
|
75
|
+
preflight binary check and read the on-disk runtime mapping).
|
|
76
|
+
Subsequent calls return the same instance.
|
|
77
|
+
|
|
78
|
+
Raises
|
|
79
|
+
------
|
|
80
|
+
ValueError
|
|
81
|
+
``name`` is not in :data:`SUPPORTED_BACKENDS`.
|
|
82
|
+
"""
|
|
83
|
+
backend = _singletons.get(name)
|
|
84
|
+
if backend is None:
|
|
85
|
+
if name == "tmux-claude-code":
|
|
86
|
+
from .tmux_claude_code import TmuxClaudeCodeBackend
|
|
87
|
+
backend = TmuxClaudeCodeBackend()
|
|
88
|
+
elif name == "tmux-codex":
|
|
89
|
+
from .tmux_codex import TmuxCodexBackend
|
|
90
|
+
backend = TmuxCodexBackend()
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"unknown agent backend: {name!r}. Supported: {SUPPORTED_BACKENDS}"
|
|
94
|
+
)
|
|
95
|
+
_singletons[name] = backend
|
|
96
|
+
return backend
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def reset() -> None:
|
|
100
|
+
"""Drop all cached singleton backends.
|
|
101
|
+
|
|
102
|
+
Mostly useful for tests — production code rarely needs this. After
|
|
103
|
+
``reset()``, the next :func:`get` call constructs a fresh backend
|
|
104
|
+
(which re-reads the runtime JSON and re-runs preflight).
|
|
105
|
+
"""
|
|
106
|
+
_singletons.clear()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = [
|
|
110
|
+
"__version__",
|
|
111
|
+
"AgentBackend",
|
|
112
|
+
"AimaxConfig",
|
|
113
|
+
"TmuxAgentsConfig",
|
|
114
|
+
"SUPPORTED_BACKENDS",
|
|
115
|
+
"config",
|
|
116
|
+
"get",
|
|
117
|
+
"get_config",
|
|
118
|
+
"set_config",
|
|
119
|
+
"reset_config",
|
|
120
|
+
"reset",
|
|
121
|
+
]
|
aimax/_version.py
ADDED
aimax/base.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""``AgentBackend`` base class — async lock + event bus + persistence.
|
|
2
|
+
|
|
3
|
+
Every concrete backend (``TmuxClaudeCodeBackend``, ``TmuxCodexBackend``)
|
|
4
|
+
extends :class:`AgentBackend`. The base class supplies three shared
|
|
5
|
+
concerns:
|
|
6
|
+
|
|
7
|
+
#. **Per-session async lock**. Mutating operations (create, send, pause,
|
|
8
|
+
terminate) on the same session are serialised; cross-session
|
|
9
|
+
operations stay concurrent.
|
|
10
|
+
#. **Event bus**. The backend dispatches each parsed JSONL entry into a
|
|
11
|
+
tiny per-session pub-sub bus, so any number of subscribers can stream
|
|
12
|
+
the agent's thoughts in real time.
|
|
13
|
+
#. **Two-tier persistence**. A *runtime* JSON file holds the mapping for
|
|
14
|
+
currently live sessions (cleared on terminate). An *archive* JSON
|
|
15
|
+
file keeps every session that has ever started so historic JSONL
|
|
16
|
+
paths can be resolved even after the live entry is gone.
|
|
17
|
+
|
|
18
|
+
Subclass contract
|
|
19
|
+
-----------------
|
|
20
|
+
|
|
21
|
+
Subclasses **must** implement:
|
|
22
|
+
|
|
23
|
+
* ``async create_new_session(opts) -> dict``
|
|
24
|
+
* ``async pause_current_and_resume_from_session(opts) -> None``
|
|
25
|
+
* ``async no_pause_current_and_queue_query_at_session(opts) -> None``
|
|
26
|
+
* ``async terminate_session(session_id) -> dict``
|
|
27
|
+
* ``is_alive(session_id) -> bool``
|
|
28
|
+
* ``is_working(session_id) -> bool``
|
|
29
|
+
* ``list_sessions() -> list[dict]``
|
|
30
|
+
|
|
31
|
+
Subclasses **may** override (defaults are sensible no-ops):
|
|
32
|
+
|
|
33
|
+
* ``get_history(session_id) -> {entries, sentinel}``
|
|
34
|
+
* ``get_agent_raw_thought_stream(session_id, listener, opts) -> unsubscribe()``
|
|
35
|
+
* ``is_job_goal_accomplished(session_id) -> bool``
|
|
36
|
+
* ``is_failed(session_id) -> bool``
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import asyncio
|
|
42
|
+
import json
|
|
43
|
+
import os
|
|
44
|
+
import threading
|
|
45
|
+
from collections import defaultdict
|
|
46
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Tiny lock-free event emitter (channel-based fanout)
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
class _EventEmitter:
|
|
53
|
+
"""In-process pub/sub. Per-channel listener list, fanout by ``emit``.
|
|
54
|
+
|
|
55
|
+
Listeners are called *synchronously* from inside ``emit``; any
|
|
56
|
+
exception raised by a listener is caught and logged so a single
|
|
57
|
+
misbehaving subscriber cannot break the others.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self._listeners: Dict[str, List[Callable[..., Any]]] = defaultdict(list)
|
|
62
|
+
self._lock = threading.Lock()
|
|
63
|
+
|
|
64
|
+
def on(self, channel: str, listener: Callable[..., Any]) -> None:
|
|
65
|
+
with self._lock:
|
|
66
|
+
self._listeners[channel].append(listener)
|
|
67
|
+
|
|
68
|
+
def off(self, channel: str, listener: Callable[..., Any]) -> None:
|
|
69
|
+
with self._lock:
|
|
70
|
+
try:
|
|
71
|
+
self._listeners[channel].remove(listener)
|
|
72
|
+
except ValueError:
|
|
73
|
+
pass # already removed — fine.
|
|
74
|
+
|
|
75
|
+
def emit(self, channel: str, *args, **kwargs) -> None:
|
|
76
|
+
# Copy under the lock so concurrent ``on``/``off`` calls do not
|
|
77
|
+
# break iteration. Listeners themselves run *outside* the lock.
|
|
78
|
+
with self._lock:
|
|
79
|
+
listeners = list(self._listeners.get(channel, ()))
|
|
80
|
+
for ln in listeners:
|
|
81
|
+
try:
|
|
82
|
+
ln(*args, **kwargs)
|
|
83
|
+
except Exception as e: # pragma: no cover — defensive
|
|
84
|
+
print(f"[event-emitter] listener on {channel} raised: {e}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# AgentBackend
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
class AgentBackend:
|
|
91
|
+
"""Shared infrastructure for every concrete backend.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
name:
|
|
96
|
+
Logical backend name (``"tmux-claude-code"``, ``"tmux-codex"``).
|
|
97
|
+
Used in log prefixes and as the key for admin defaults.
|
|
98
|
+
runtime_file:
|
|
99
|
+
Absolute path to the *live* mapping JSON. Removed on terminate.
|
|
100
|
+
archive_file:
|
|
101
|
+
Absolute path to the *all-time* mapping JSON. Never removed; the
|
|
102
|
+
live entries are mirrored here at write time so historic JSONL
|
|
103
|
+
paths can be looked up after admin closes a window. Pass
|
|
104
|
+
``None`` to disable archiving.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
name: str,
|
|
110
|
+
runtime_file: str | os.PathLike[str],
|
|
111
|
+
archive_file: Optional[str | os.PathLike[str]] = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
self.name = name
|
|
114
|
+
self.runtime_file = str(runtime_file)
|
|
115
|
+
self.archive_file = str(archive_file) if archive_file else None
|
|
116
|
+
|
|
117
|
+
# ``locks`` is keyed by ``session_id``; created lazily by
|
|
118
|
+
# :meth:`_get_lock`.
|
|
119
|
+
self.locks: Dict[str, asyncio.Lock] = {}
|
|
120
|
+
|
|
121
|
+
self.emitter = _EventEmitter()
|
|
122
|
+
|
|
123
|
+
self.persisted: Dict[str, dict] = self._load_json(self.runtime_file)
|
|
124
|
+
self.archive: Dict[str, dict] = (
|
|
125
|
+
self._load_json(self.archive_file) if self.archive_file else {}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# One-shot catch-up: when the archive feature was added, any
|
|
129
|
+
# already-running sessions only existed in ``persisted``. Copy
|
|
130
|
+
# them across so historic jsonl-path lookups work for them too.
|
|
131
|
+
if self.archive_file:
|
|
132
|
+
dirty = False
|
|
133
|
+
for sid, p in list(self.persisted.items()):
|
|
134
|
+
if sid not in self.archive:
|
|
135
|
+
self.archive[sid] = dict(p)
|
|
136
|
+
dirty = True
|
|
137
|
+
if dirty:
|
|
138
|
+
self._save_archive()
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
# Per-session asyncio lock
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
def _get_lock(self, session_id: Optional[str]) -> asyncio.Lock:
|
|
144
|
+
# ``""`` is the catch-all key for cases where the caller did not
|
|
145
|
+
# supply a session id (which the concrete backends rarely do, but
|
|
146
|
+
# we tolerate it rather than crashing).
|
|
147
|
+
key = session_id or ""
|
|
148
|
+
lock = self.locks.get(key)
|
|
149
|
+
if lock is None:
|
|
150
|
+
lock = asyncio.Lock()
|
|
151
|
+
self.locks[key] = lock
|
|
152
|
+
return lock
|
|
153
|
+
|
|
154
|
+
async def _with_lock(
|
|
155
|
+
self, session_id: Optional[str], fn: Callable[[], Awaitable[Any]]
|
|
156
|
+
) -> Any:
|
|
157
|
+
"""Run ``fn()`` exclusively for the given session."""
|
|
158
|
+
async with self._get_lock(session_id):
|
|
159
|
+
return await fn()
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
# Event subscription
|
|
163
|
+
# ------------------------------------------------------------------
|
|
164
|
+
def get_agent_raw_thought_stream(
|
|
165
|
+
self,
|
|
166
|
+
session_id: str,
|
|
167
|
+
listener: Callable[..., Any],
|
|
168
|
+
opts: Optional[dict] = None,
|
|
169
|
+
) -> Callable[[], None]:
|
|
170
|
+
"""Subscribe to the live raw-event stream for ``session_id``.
|
|
171
|
+
|
|
172
|
+
``opts`` is reserved for subclass-specific cues — e.g. the tmux
|
|
173
|
+
backends understand ``{"fromSentinel": <byte_offset>}`` to splice
|
|
174
|
+
history and live into a single duplicate-free stream.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
A zero-argument callable that, when invoked, unsubscribes the
|
|
179
|
+
listener. Idempotent.
|
|
180
|
+
"""
|
|
181
|
+
ch = f"raw:{session_id}"
|
|
182
|
+
self.emitter.on(ch, listener)
|
|
183
|
+
|
|
184
|
+
def unsubscribe() -> None:
|
|
185
|
+
self.emitter.off(ch, listener)
|
|
186
|
+
|
|
187
|
+
return unsubscribe
|
|
188
|
+
|
|
189
|
+
def _emit_raw(self, session_id: str, raw: Any) -> None:
|
|
190
|
+
"""Fan ``raw`` out to every subscriber of the given session."""
|
|
191
|
+
self.emitter.emit(f"raw:{session_id}", raw)
|
|
192
|
+
|
|
193
|
+
def get_history(self, session_id: str) -> dict:
|
|
194
|
+
"""Return ``{entries: [...], sentinel: ...}`` for ``session_id``.
|
|
195
|
+
|
|
196
|
+
The default returns an empty snapshot — subclasses override using
|
|
197
|
+
their own JSONL files. The sentinel is whatever the subclass
|
|
198
|
+
uses as a "tail from here" cursor; for the tmux backends it is
|
|
199
|
+
the file byte size.
|
|
200
|
+
"""
|
|
201
|
+
return {"entries": [], "sentinel": None}
|
|
202
|
+
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
# Persistence
|
|
205
|
+
# ------------------------------------------------------------------
|
|
206
|
+
def _load_json(self, file: Optional[str]) -> dict:
|
|
207
|
+
"""Read a JSON file. Returns ``{}`` for missing/corrupt files."""
|
|
208
|
+
if not file:
|
|
209
|
+
return {}
|
|
210
|
+
try:
|
|
211
|
+
if not os.path.exists(file):
|
|
212
|
+
return {}
|
|
213
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
214
|
+
return json.load(f) or {}
|
|
215
|
+
except Exception as e: # pragma: no cover — defensive
|
|
216
|
+
print(f"[agents/{self.name}] load {os.path.basename(file)} failed: {e}")
|
|
217
|
+
return {}
|
|
218
|
+
|
|
219
|
+
def _save_json(self, file: str, obj: dict) -> None:
|
|
220
|
+
try:
|
|
221
|
+
os.makedirs(os.path.dirname(file), exist_ok=True)
|
|
222
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
223
|
+
json.dump(obj, f, indent=2)
|
|
224
|
+
except Exception as e: # pragma: no cover — defensive
|
|
225
|
+
print(f"[agents/{self.name}] save {os.path.basename(file)} failed: {e}")
|
|
226
|
+
|
|
227
|
+
def _save_persisted(self) -> None:
|
|
228
|
+
self._save_json(self.runtime_file, self.persisted)
|
|
229
|
+
|
|
230
|
+
def _save_archive(self) -> None:
|
|
231
|
+
if self.archive_file:
|
|
232
|
+
self._save_json(self.archive_file, self.archive)
|
|
233
|
+
|
|
234
|
+
def _persist_entry(self, session_id: str, partial: dict) -> None:
|
|
235
|
+
"""Merge ``partial`` into the entry for ``session_id`` and save both files."""
|
|
236
|
+
merged = {**(self.persisted.get(session_id) or {}), **partial}
|
|
237
|
+
self.persisted[session_id] = merged
|
|
238
|
+
self._save_persisted()
|
|
239
|
+
if self.archive_file:
|
|
240
|
+
arch = {**(self.archive.get(session_id) or {}), **partial}
|
|
241
|
+
self.archive[session_id] = arch
|
|
242
|
+
self._save_archive()
|
|
243
|
+
|
|
244
|
+
def _forget_persisted(self, session_id: str) -> None:
|
|
245
|
+
"""Remove ``session_id`` from ``persisted`` only — archive is preserved."""
|
|
246
|
+
self.persisted.pop(session_id, None)
|
|
247
|
+
self._save_persisted()
|
|
248
|
+
|
|
249
|
+
def _lookup_archived_jsonl_path(self, session_id: str) -> Optional[str]:
|
|
250
|
+
"""Fall back to the archive when the live map has no JSONL path.
|
|
251
|
+
|
|
252
|
+
This is the safety net for the "admin closed the window then a
|
|
253
|
+
client asks for history" case.
|
|
254
|
+
"""
|
|
255
|
+
entry = self.archive.get(session_id) if self.archive else None
|
|
256
|
+
return (entry or {}).get("jsonlPath")
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
# Misc helpers
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
def get_session_use_proxy(self, session_id: str) -> Optional[bool]:
|
|
262
|
+
"""Return the proxy choice persisted for ``session_id``, or ``None`` if unknown.
|
|
263
|
+
|
|
264
|
+
Reads the in-memory runtime map first (a subclass attribute
|
|
265
|
+
named ``runtime``, if present), then falls back to the on-disk
|
|
266
|
+
persisted map.
|
|
267
|
+
"""
|
|
268
|
+
runtime_entry = None
|
|
269
|
+
rt = getattr(self, "runtime", None)
|
|
270
|
+
if rt is not None and hasattr(rt, "get"):
|
|
271
|
+
runtime_entry = rt.get(session_id)
|
|
272
|
+
entry = runtime_entry or (self.persisted.get(session_id) if self.persisted else None)
|
|
273
|
+
if not entry:
|
|
274
|
+
return None
|
|
275
|
+
value = entry.get("useProxy")
|
|
276
|
+
if value is None:
|
|
277
|
+
value = entry.get("use_proxy")
|
|
278
|
+
if value in (True, 1, "1", "true"):
|
|
279
|
+
return True
|
|
280
|
+
if value in (False, 0, "0", "false"):
|
|
281
|
+
return False
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
# Default state queries (subclass overrides almost always)
|
|
286
|
+
# ------------------------------------------------------------------
|
|
287
|
+
def is_working(self, session_id: str) -> bool: # noqa: ARG002
|
|
288
|
+
"""Is the agent currently mid-turn? Default ``False`` (don't know)."""
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
def is_job_goal_accomplished(self, session_id: str) -> bool: # noqa: ARG002
|
|
292
|
+
"""Did the agent's overall job finish?
|
|
293
|
+
|
|
294
|
+
Convention: the backend writes ``running.flag`` on session start,
|
|
295
|
+
the agent removes it when its job ends. The flag's *absence* is
|
|
296
|
+
the signal. With no cwd context the base class returns ``False``.
|
|
297
|
+
"""
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
def is_failed(self, session_id: str) -> bool: # noqa: ARG002
|
|
301
|
+
"""Did the agent's job fail? Mirror of :meth:`is_job_goal_accomplished`."""
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
__all__ = ["AgentBackend"]
|