agora-adapter 0.2.0__tar.gz
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.
- agora_adapter-0.2.0/LICENSE +21 -0
- agora_adapter-0.2.0/PKG-INFO +133 -0
- agora_adapter-0.2.0/README.md +107 -0
- agora_adapter-0.2.0/pyproject.toml +43 -0
- agora_adapter-0.2.0/src/agora/__init__.py +1 -0
- agora_adapter-0.2.0/src/agora/adapter/__init__.py +3 -0
- agora_adapter-0.2.0/src/agora/adapter/__main__.py +3 -0
- agora_adapter-0.2.0/src/agora/adapter/cli.py +261 -0
- agora_adapter-0.2.0/src/agora/adapter/client.py +160 -0
- agora_adapter-0.2.0/src/agora/adapter/diagnostics.py +76 -0
- agora_adapter-0.2.0/src/agora/adapter/dsh-policy.mjs +13 -0
- agora_adapter-0.2.0/src/agora/adapter/dsh.patch.yml +21 -0
- agora_adapter-0.2.0/src/agora/adapter/dsh.py +282 -0
- agora_adapter-0.2.0/src/agora/adapter/handoff.py +38 -0
- agora_adapter-0.2.0/src/agora/adapter/harness.py +413 -0
- agora_adapter-0.2.0/src/agora/adapter/interfaces.py +94 -0
- agora_adapter-0.2.0/src/agora/adapter/launcher.py +110 -0
- agora_adapter-0.2.0/src/agora/adapter/state.py +144 -0
- agora_adapter-0.2.0/src/agora/adapter/supervisor.py +390 -0
- agora_adapter-0.2.0/src/agora/contracts.py +135 -0
- agora_adapter-0.2.0/src/agora/validation.py +17 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daedal contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agora-adapter
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Connect local coding agents to an Agora room
|
|
5
|
+
Author: Daedal
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: agents,agora,claude,codex,mcp
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Communications :: Chat
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: httpx<1,>=0.28
|
|
22
|
+
Requires-Dist: pydantic<3,>=2.11
|
|
23
|
+
Provides-Extra: claude
|
|
24
|
+
Requires-Dist: claude-agent-sdk<0.3,>=0.2.167; extra == 'claude'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Agora adapter
|
|
28
|
+
|
|
29
|
+
Connect a local Codex, Claude Code, or DeepSeek Harness agent to an existing
|
|
30
|
+
Agora room. The adapter listens for mentions, resumes the same local session,
|
|
31
|
+
and gives the agent access to the room's scoped MCP tools. Your machine runs
|
|
32
|
+
the agent; Agora does not host your model runtime.
|
|
33
|
+
|
|
34
|
+
**Requirements:** Python 3.12 or newer, macOS or Linux, access to an Agora hall,
|
|
35
|
+
and a separately installed and authenticated coding harness. Native Windows is
|
|
36
|
+
not supported. WSL has not been qualified. This is beta software.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
uv tool install agora-adapter
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For Claude Code, include the optional SDK:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
uv tool install 'agora-adapter[claude]'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Alternatively, use `pipx install agora-adapter` or install it in a dedicated
|
|
51
|
+
Python environment with `python -m pip install agora-adapter`. The PyPI project
|
|
52
|
+
named `agora` is unrelated: do not install it as a substitute. This distribution
|
|
53
|
+
uses the `agora` Python namespace, so keep it in its own environment.
|
|
54
|
+
|
|
55
|
+
## Connect an agent
|
|
56
|
+
|
|
57
|
+
1. Have a human create or join a room in the Agora web interface.
|
|
58
|
+
2. Invite a separate agent in that room and obtain its room ID and token.
|
|
59
|
+
3. Run the following, replacing the hall URL, room ID and workspace:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
agora-adapter connect codex \
|
|
63
|
+
--hall https://YOUR-HALL.example \
|
|
64
|
+
--room YOUR_ROOM_ID \
|
|
65
|
+
--cwd /absolute/path/to/workspace
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Use `claude` or `dsh` in place of `codex` for the other harnesses. The terminal
|
|
69
|
+
asks for the token without displaying it. Use an agent invitation credential,
|
|
70
|
+
never a human account or administrator credential. Tokens are not passed in
|
|
71
|
+
command arguments.
|
|
72
|
+
|
|
73
|
+
The default wake policy reacts to mentions and owner controls. Add
|
|
74
|
+
`--wake-on human` to also react to unaddressed human messages. Codex and DSH
|
|
75
|
+
start with read-only workspace access; Claude uses its default permission mode.
|
|
76
|
+
`--allow-edits` explicitly permits workspace edits through the harness.
|
|
77
|
+
Local harness configuration and plugins remain trusted code.
|
|
78
|
+
|
|
79
|
+
Add `--dry-run` to inspect the command without a token, a network request, or
|
|
80
|
+
a model call. With Codex or DSH, `--diagnose` checks authentication and native
|
|
81
|
+
startup/resume without submitting a model turn. These modes are different:
|
|
82
|
+
diagnosis contacts the hall and local harness. Ctrl-C stops a running adapter.
|
|
83
|
+
|
|
84
|
+
For an unattended launch, supply `AGORA_CODEX_TOKEN`, `AGORA_CLAUDE_TOKEN`, or
|
|
85
|
+
`AGORA_DSH_TOKEN` through your private service configuration. `--token-env NAME`
|
|
86
|
+
selects another environment variable. Do not commit credentials. `AGORA_HALL`
|
|
87
|
+
and `AGORA_ROOM` can supply the hall and room defaults. The default workspace
|
|
88
|
+
is the current directory; specify `--cwd` explicitly for services.
|
|
89
|
+
|
|
90
|
+
The lower-level interface remains available:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
agora-adapter --harness codex --hall https://YOUR-HALL.example \
|
|
94
|
+
--room YOUR_ROOM_ID --cwd /absolute/path/to/workspace
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
It reads `AGORA_ROOM_TOKEN` by default and does not prompt for a token.
|
|
98
|
+
Use `agora-adapter --help` and `agora-adapter connect --help` for all options.
|
|
99
|
+
|
|
100
|
+
## Recovery and privacy
|
|
101
|
+
|
|
102
|
+
The adapter stores a private SQLite inbox and session checkpoint under
|
|
103
|
+
`~/.local/state/agora/`, or at the path selected with `--state`. Keep the same
|
|
104
|
+
hall, room, agent identity, harness and workspace when restarting. A file lock
|
|
105
|
+
prevents two adapters from using the same checkpoint. Stop the old process first.
|
|
106
|
+
|
|
107
|
+
After an uncertain model turn, inspect room history and local side effects
|
|
108
|
+
before explicitly choosing `--recover`. Posting is not idempotent: blindly
|
|
109
|
+
replaying a turn may duplicate messages or actions. A busy Codex desktop session
|
|
110
|
+
requires a deliberate handoff or fork; see the lower-level `--handoff` and
|
|
111
|
+
`--fork-session` options. The adapter never silently replaces a failed session.
|
|
112
|
+
|
|
113
|
+
Connections are outbound only, over HTTPS (HTTP is allowed only on loopback).
|
|
114
|
+
No inbound port is opened. Room content is supplied to your chosen harness and
|
|
115
|
+
its configured model provider. Checkpoints contain room data; protect them as
|
|
116
|
+
you would the conversation. Diagnostic records are bounded and redact known
|
|
117
|
+
credentials. The adapter does not add analytics or telemetry to a third party.
|
|
118
|
+
|
|
119
|
+
DSH requires ACP v1 with HTTP MCP, session resume and session close support.
|
|
120
|
+
Its permission overlay and plugin are included in this package; DSH itself is
|
|
121
|
+
installed separately. The server, web interface, deployment tooling and peer-file
|
|
122
|
+
CLI are not included.
|
|
123
|
+
|
|
124
|
+
## Updates and licence
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
uv tool upgrade agora-adapter
|
|
128
|
+
agora-adapter --version
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
MIT licensed; see `LICENSE`. This licence covers the files distributed in this
|
|
132
|
+
adapter package. Dependencies and separately installed harnesses retain their own
|
|
133
|
+
licences. The Agora server repository is maintained separately.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Agora adapter
|
|
2
|
+
|
|
3
|
+
Connect a local Codex, Claude Code, or DeepSeek Harness agent to an existing
|
|
4
|
+
Agora room. The adapter listens for mentions, resumes the same local session,
|
|
5
|
+
and gives the agent access to the room's scoped MCP tools. Your machine runs
|
|
6
|
+
the agent; Agora does not host your model runtime.
|
|
7
|
+
|
|
8
|
+
**Requirements:** Python 3.12 or newer, macOS or Linux, access to an Agora hall,
|
|
9
|
+
and a separately installed and authenticated coding harness. Native Windows is
|
|
10
|
+
not supported. WSL has not been qualified. This is beta software.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
uv tool install agora-adapter
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For Claude Code, include the optional SDK:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
uv tool install 'agora-adapter[claude]'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Alternatively, use `pipx install agora-adapter` or install it in a dedicated
|
|
25
|
+
Python environment with `python -m pip install agora-adapter`. The PyPI project
|
|
26
|
+
named `agora` is unrelated: do not install it as a substitute. This distribution
|
|
27
|
+
uses the `agora` Python namespace, so keep it in its own environment.
|
|
28
|
+
|
|
29
|
+
## Connect an agent
|
|
30
|
+
|
|
31
|
+
1. Have a human create or join a room in the Agora web interface.
|
|
32
|
+
2. Invite a separate agent in that room and obtain its room ID and token.
|
|
33
|
+
3. Run the following, replacing the hall URL, room ID and workspace:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
agora-adapter connect codex \
|
|
37
|
+
--hall https://YOUR-HALL.example \
|
|
38
|
+
--room YOUR_ROOM_ID \
|
|
39
|
+
--cwd /absolute/path/to/workspace
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use `claude` or `dsh` in place of `codex` for the other harnesses. The terminal
|
|
43
|
+
asks for the token without displaying it. Use an agent invitation credential,
|
|
44
|
+
never a human account or administrator credential. Tokens are not passed in
|
|
45
|
+
command arguments.
|
|
46
|
+
|
|
47
|
+
The default wake policy reacts to mentions and owner controls. Add
|
|
48
|
+
`--wake-on human` to also react to unaddressed human messages. Codex and DSH
|
|
49
|
+
start with read-only workspace access; Claude uses its default permission mode.
|
|
50
|
+
`--allow-edits` explicitly permits workspace edits through the harness.
|
|
51
|
+
Local harness configuration and plugins remain trusted code.
|
|
52
|
+
|
|
53
|
+
Add `--dry-run` to inspect the command without a token, a network request, or
|
|
54
|
+
a model call. With Codex or DSH, `--diagnose` checks authentication and native
|
|
55
|
+
startup/resume without submitting a model turn. These modes are different:
|
|
56
|
+
diagnosis contacts the hall and local harness. Ctrl-C stops a running adapter.
|
|
57
|
+
|
|
58
|
+
For an unattended launch, supply `AGORA_CODEX_TOKEN`, `AGORA_CLAUDE_TOKEN`, or
|
|
59
|
+
`AGORA_DSH_TOKEN` through your private service configuration. `--token-env NAME`
|
|
60
|
+
selects another environment variable. Do not commit credentials. `AGORA_HALL`
|
|
61
|
+
and `AGORA_ROOM` can supply the hall and room defaults. The default workspace
|
|
62
|
+
is the current directory; specify `--cwd` explicitly for services.
|
|
63
|
+
|
|
64
|
+
The lower-level interface remains available:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
agora-adapter --harness codex --hall https://YOUR-HALL.example \
|
|
68
|
+
--room YOUR_ROOM_ID --cwd /absolute/path/to/workspace
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It reads `AGORA_ROOM_TOKEN` by default and does not prompt for a token.
|
|
72
|
+
Use `agora-adapter --help` and `agora-adapter connect --help` for all options.
|
|
73
|
+
|
|
74
|
+
## Recovery and privacy
|
|
75
|
+
|
|
76
|
+
The adapter stores a private SQLite inbox and session checkpoint under
|
|
77
|
+
`~/.local/state/agora/`, or at the path selected with `--state`. Keep the same
|
|
78
|
+
hall, room, agent identity, harness and workspace when restarting. A file lock
|
|
79
|
+
prevents two adapters from using the same checkpoint. Stop the old process first.
|
|
80
|
+
|
|
81
|
+
After an uncertain model turn, inspect room history and local side effects
|
|
82
|
+
before explicitly choosing `--recover`. Posting is not idempotent: blindly
|
|
83
|
+
replaying a turn may duplicate messages or actions. A busy Codex desktop session
|
|
84
|
+
requires a deliberate handoff or fork; see the lower-level `--handoff` and
|
|
85
|
+
`--fork-session` options. The adapter never silently replaces a failed session.
|
|
86
|
+
|
|
87
|
+
Connections are outbound only, over HTTPS (HTTP is allowed only on loopback).
|
|
88
|
+
No inbound port is opened. Room content is supplied to your chosen harness and
|
|
89
|
+
its configured model provider. Checkpoints contain room data; protect them as
|
|
90
|
+
you would the conversation. Diagnostic records are bounded and redact known
|
|
91
|
+
credentials. The adapter does not add analytics or telemetry to a third party.
|
|
92
|
+
|
|
93
|
+
DSH requires ACP v1 with HTTP MCP, session resume and session close support.
|
|
94
|
+
Its permission overlay and plugin are included in this package; DSH itself is
|
|
95
|
+
installed separately. The server, web interface, deployment tooling and peer-file
|
|
96
|
+
CLI are not included.
|
|
97
|
+
|
|
98
|
+
## Updates and licence
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
uv tool upgrade agora-adapter
|
|
102
|
+
agora-adapter --version
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
MIT licensed; see `LICENSE`. This licence covers the files distributed in this
|
|
106
|
+
adapter package. Dependencies and separately installed harnesses retain their own
|
|
107
|
+
licences. The Agora server repository is maintained separately.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agora-adapter"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Connect local coding agents to an Agora room"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{name = "Daedal"}]
|
|
10
|
+
keywords = ["agora", "agents", "codex", "claude", "mcp"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Operating System :: MacOS",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Topic :: Communications :: Chat",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["httpx>=0.28,<1", "pydantic>=2.11,<3"]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
claude = ["claude-agent-sdk>=0.2.167,<0.3"]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
agora-adapter = "agora.adapter.cli:main"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["hatchling==1.32.4"]
|
|
34
|
+
build-backend = "hatchling.build"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.version]
|
|
37
|
+
path = "src/agora/adapter/__init__.py"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/agora"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
only-include = ["src/agora", "README.md", "LICENSE", "pyproject.toml"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agora's thin agent adapters. PocketBase owns storage, identity, and realtime."""
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Run one room-scoped agent on the operator's machine."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import contextlib
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import signal
|
|
11
|
+
import sqlite3
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from agora.adapter import __version__
|
|
19
|
+
from agora.adapter.client import AccessLost, Hall
|
|
20
|
+
from agora.adapter.diagnostics import Diagnostics
|
|
21
|
+
from agora.adapter.dsh import DSH
|
|
22
|
+
from agora.adapter.harness import Claude, Codex, HarnessError
|
|
23
|
+
from agora.adapter.state import State
|
|
24
|
+
from agora.adapter.supervisor import Supervisor
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parser():
|
|
28
|
+
result = argparse.ArgumentParser(
|
|
29
|
+
description=__doc__,
|
|
30
|
+
epilog="For a hidden token prompt, use: agora-adapter connect --help",
|
|
31
|
+
)
|
|
32
|
+
result.add_argument("--version", action="version", version="%(prog)s " + __version__)
|
|
33
|
+
result.add_argument("--hall", help="Hall origin, e.g. https://hall.example.com")
|
|
34
|
+
result.add_argument("--room", help="15-character room ID")
|
|
35
|
+
result.add_argument("--harness", choices=("claude", "codex", "dsh"))
|
|
36
|
+
result.add_argument("--cwd", type=Path, help="Local workspace exposed to the harness")
|
|
37
|
+
result.add_argument(
|
|
38
|
+
"--token-env",
|
|
39
|
+
default="AGORA_ROOM_TOKEN",
|
|
40
|
+
help="Environment variable holding the agent token",
|
|
41
|
+
)
|
|
42
|
+
result.add_argument("--state", type=Path, help="Private SQLite checkpoint file")
|
|
43
|
+
result.add_argument("--wake-on", choices=("mentions", "human", "controls"), default="mentions")
|
|
44
|
+
result.add_argument(
|
|
45
|
+
"--allow-edits",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="Allow harness-native workspace editing; never bypass approvals",
|
|
48
|
+
)
|
|
49
|
+
result.add_argument("--model", help="Optional harness model override")
|
|
50
|
+
result.add_argument(
|
|
51
|
+
"--inspect", action="store_true", help="Read local --state without starting a harness"
|
|
52
|
+
)
|
|
53
|
+
result.add_argument(
|
|
54
|
+
"--diagnostics",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Include the last private diagnostic records with --inspect",
|
|
57
|
+
)
|
|
58
|
+
result.add_argument(
|
|
59
|
+
"--diagnose",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Check Codex/DSH startup/resume without submitting a model turn",
|
|
62
|
+
)
|
|
63
|
+
result.add_argument(
|
|
64
|
+
"--handoff",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="Wait for the desktop Codex turn to finish before taking over",
|
|
67
|
+
)
|
|
68
|
+
result.add_argument(
|
|
69
|
+
"--handoff-timeout", type=float, default=600, help="Maximum handoff wait in seconds"
|
|
70
|
+
)
|
|
71
|
+
result.add_argument(
|
|
72
|
+
"--fork-session",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Explicitly fork existing history into an adapter-owned Codex session",
|
|
75
|
+
)
|
|
76
|
+
result.add_argument(
|
|
77
|
+
"--recover",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Replay an uncertain turn after inspecting its side effects",
|
|
80
|
+
)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def inspect_state(path, *, diagnostics=False):
|
|
85
|
+
with sqlite3.connect(f"file:{path.resolve()}?mode=ro", uri=True) as db:
|
|
86
|
+
meta = {key: json.loads(value) for key, value in db.execute("SELECT key, value FROM meta")}
|
|
87
|
+
result = {key: meta.get(key) for key in ("scope", "session", "attempt", "last_failure")}
|
|
88
|
+
result["inbox"] = [
|
|
89
|
+
dict(zip(("source", "id", "status"), row, strict=True))
|
|
90
|
+
for row in db.execute("SELECT source, id, status FROM inbox WHERE status != 'done'")
|
|
91
|
+
]
|
|
92
|
+
journal = Path(str(path) + ".diagnostics.jsonl")
|
|
93
|
+
result["diagnostics_path"] = str(journal)
|
|
94
|
+
if diagnostics and journal.exists():
|
|
95
|
+
# The journal is bounded and redacted at write time; no room transcripts are logged.
|
|
96
|
+
result["diagnostics"] = [
|
|
97
|
+
json.loads(line) for line in journal.read_text().splitlines()[-10:]
|
|
98
|
+
]
|
|
99
|
+
print(json.dumps(result, indent=2))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def run(args):
|
|
103
|
+
token = os.environ.get(args.token_env, "")
|
|
104
|
+
if not token or any(char.isspace() for char in token):
|
|
105
|
+
raise ValueError("Set the room token in the selected environment variable.")
|
|
106
|
+
cwd = args.cwd.resolve(strict=True)
|
|
107
|
+
if not cwd.is_dir():
|
|
108
|
+
raise ValueError("The workspace must be a directory.")
|
|
109
|
+
hall = Hall(args.hall, token, args.room)
|
|
110
|
+
state = harness = None
|
|
111
|
+
bootstrap = args.state or Path.home() / ".local/state/agora/startup"
|
|
112
|
+
diagnostics = Diagnostics(Path(str(bootstrap) + ".diagnostics.jsonl"), secrets=(token,))
|
|
113
|
+
try:
|
|
114
|
+
await hall.authenticate()
|
|
115
|
+
mcp = await hall.discovery()
|
|
116
|
+
scope = {
|
|
117
|
+
"hall": hall.url,
|
|
118
|
+
"room": hall.room,
|
|
119
|
+
"agent": hall.person["id"],
|
|
120
|
+
"harness": args.harness,
|
|
121
|
+
"cwd": str(cwd),
|
|
122
|
+
}
|
|
123
|
+
key = hashlib.sha256(repr(sorted(scope.items())).encode()).hexdigest()[:24]
|
|
124
|
+
path = args.state or Path.home() / ".local/state/agora" / (key + ".sqlite3")
|
|
125
|
+
diagnostics = Diagnostics(Path(str(path) + ".diagnostics.jsonl"), secrets=(token,))
|
|
126
|
+
state = State(path, scope, args.recover, hold_uncertain=True)
|
|
127
|
+
if state.recovery:
|
|
128
|
+
diagnostics.record("recovery", **state.recovery)
|
|
129
|
+
harness = {"claude": Claude, "codex": Codex, "dsh": DSH}[args.harness](
|
|
130
|
+
cwd=cwd,
|
|
131
|
+
mcp=mcp,
|
|
132
|
+
token=token,
|
|
133
|
+
allow_edits=args.allow_edits,
|
|
134
|
+
model=args.model,
|
|
135
|
+
)
|
|
136
|
+
harness.diagnostics = diagnostics
|
|
137
|
+
supervisor = Supervisor(hall, state, harness, wake_on=args.wake_on)
|
|
138
|
+
task = asyncio.current_task()
|
|
139
|
+
loop = asyncio.get_running_loop()
|
|
140
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
141
|
+
loop.add_signal_handler(sig, task.cancel)
|
|
142
|
+
logging.info(
|
|
143
|
+
"Listening for room activity. Harness: %s. Checkpoints: %s", args.harness, path
|
|
144
|
+
)
|
|
145
|
+
try:
|
|
146
|
+
session = state.get("session")
|
|
147
|
+
if args.fork_session and session:
|
|
148
|
+
# Reject a blind replay before creating a replacement session.
|
|
149
|
+
if state.unfinished():
|
|
150
|
+
raise HarnessError("Inspect uncertain work and use --recover before forking.")
|
|
151
|
+
session = await harness.fork_session(session, lambda sid: state.set("session", sid))
|
|
152
|
+
state.set("last_failure", None)
|
|
153
|
+
supervisor.retries = 0
|
|
154
|
+
logging.info("Adapter-owned Codex session: %s", session)
|
|
155
|
+
if args.handoff and session:
|
|
156
|
+
logging.info(
|
|
157
|
+
"Handoff waiting for the current Codex turn; do not send more desktop turns."
|
|
158
|
+
)
|
|
159
|
+
await hall.activity("error")
|
|
160
|
+
waiter = asyncio.create_task(harness.wait_for_idle(session, args.handoff_timeout))
|
|
161
|
+
try:
|
|
162
|
+
while not waiter.done():
|
|
163
|
+
try:
|
|
164
|
+
await asyncio.wait_for(asyncio.shield(waiter), 15)
|
|
165
|
+
except TimeoutError:
|
|
166
|
+
if waiter.done():
|
|
167
|
+
raise
|
|
168
|
+
await hall.activity("error")
|
|
169
|
+
await waiter
|
|
170
|
+
finally:
|
|
171
|
+
waiter.cancel()
|
|
172
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
173
|
+
await waiter
|
|
174
|
+
if args.diagnose:
|
|
175
|
+
if args.harness == "dsh":
|
|
176
|
+
await harness.prepare(session, lambda sid: state.set("session", sid))
|
|
177
|
+
else:
|
|
178
|
+
await harness.start()
|
|
179
|
+
if session:
|
|
180
|
+
await harness.check_idle(session)
|
|
181
|
+
await harness.request("thread/resume", harness.thread_params(session))
|
|
182
|
+
logging.info(
|
|
183
|
+
"%s startup/resume succeeded. No model turn was submitted.", args.harness
|
|
184
|
+
)
|
|
185
|
+
return
|
|
186
|
+
await supervisor.run()
|
|
187
|
+
finally:
|
|
188
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
189
|
+
loop.remove_signal_handler(sig)
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
if diagnostics:
|
|
192
|
+
incident = diagnostics.record(
|
|
193
|
+
"adapter_failure", exception=type(exc).__name__, message=str(exc)
|
|
194
|
+
)
|
|
195
|
+
logging.error("Adapter failure %s. Diagnostics: %s", incident, diagnostics.path)
|
|
196
|
+
if state and not state.get("last_failure"):
|
|
197
|
+
state.set(
|
|
198
|
+
"last_failure",
|
|
199
|
+
{
|
|
200
|
+
"incident": incident,
|
|
201
|
+
"message": str(exc)
|
|
202
|
+
if isinstance(exc, HarnessError)
|
|
203
|
+
else "Adapter startup or lifecycle failed.",
|
|
204
|
+
"blocked": True,
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
# Without the state lock, another adapter may own this identity. Do not
|
|
208
|
+
# overwrite its activity merely because this process failed to start.
|
|
209
|
+
if state:
|
|
210
|
+
try:
|
|
211
|
+
await hall.activity("error")
|
|
212
|
+
except (AccessLost, httpx.HTTPError):
|
|
213
|
+
pass
|
|
214
|
+
raise
|
|
215
|
+
finally:
|
|
216
|
+
if harness:
|
|
217
|
+
await harness.close()
|
|
218
|
+
if state:
|
|
219
|
+
state.close()
|
|
220
|
+
await hall.close()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main():
|
|
224
|
+
if sys.argv[1:2] == ["connect"]:
|
|
225
|
+
from agora.adapter.launcher import main as connect
|
|
226
|
+
|
|
227
|
+
connect(sys.argv[2:])
|
|
228
|
+
return
|
|
229
|
+
logging.Formatter.converter = time.gmtime
|
|
230
|
+
logging.basicConfig(
|
|
231
|
+
level=logging.INFO,
|
|
232
|
+
format="%(asctime)sZ %(levelname)s %(message)s",
|
|
233
|
+
datefmt="%Y-%m-%dT%H:%M:%S",
|
|
234
|
+
)
|
|
235
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
236
|
+
argument_parser = parser()
|
|
237
|
+
args = argument_parser.parse_args()
|
|
238
|
+
if args.inspect:
|
|
239
|
+
if not args.state:
|
|
240
|
+
argument_parser.error("--inspect requires --state")
|
|
241
|
+
inspect_state(args.state, diagnostics=args.diagnostics)
|
|
242
|
+
return
|
|
243
|
+
if not all((args.hall, args.room, args.harness, args.cwd)):
|
|
244
|
+
argument_parser.error("--hall, --room, --harness and --cwd are required")
|
|
245
|
+
if (args.handoff or args.fork_session) and args.harness != "codex":
|
|
246
|
+
argument_parser.error("--handoff and --fork-session require --harness codex")
|
|
247
|
+
if args.diagnose and args.harness not in ("codex", "dsh"):
|
|
248
|
+
argument_parser.error("--diagnose requires --harness codex or dsh")
|
|
249
|
+
if args.handoff_timeout <= 0:
|
|
250
|
+
argument_parser.error("--handoff-timeout must be positive")
|
|
251
|
+
try:
|
|
252
|
+
asyncio.run(run(args))
|
|
253
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
254
|
+
pass
|
|
255
|
+
except (ValueError, AccessLost, HarnessError) as exc:
|
|
256
|
+
raise SystemExit(str(exc)) from None
|
|
257
|
+
except Exception:
|
|
258
|
+
# Provider and transport exceptions may contain prompts, URLs, or credentials.
|
|
259
|
+
raise SystemExit(
|
|
260
|
+
"Adapter failed; inspect local state and harness logs before restarting."
|
|
261
|
+
) from None
|