@yul-labs/agent-relay 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +395 -0
- package/agent-relay.config.example.json +38 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2938 -0
- package/dist/index.d.ts +1698 -0
- package/dist/index.js +2571 -0
- package/package.json +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agent-relay 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.
|
package/README.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# agent-relay
|
|
2
|
+
|
|
3
|
+
> A vendor-agnostic **interactive** LLM coding-agent session orchestrator.
|
|
4
|
+
|
|
5
|
+
`agent-relay` runs a coding agent (**Claude Code**, **Codex**) in its real
|
|
6
|
+
interactive terminal, watches for the approval / choice prompts it raises, and a
|
|
7
|
+
pluggable **Decider** (a rule policy, an LLM, or any function/API) answers them —
|
|
8
|
+
so the agent runs **unattended without you sitting at the keyboard**. Every run
|
|
9
|
+
is tracked as a *session* with a log, and completion is detected so success can
|
|
10
|
+
be judged.
|
|
11
|
+
|
|
12
|
+
This is the core idea: **don't suppress the agent's prompts — let them happen and
|
|
13
|
+
have an LLM/policy answer them**, interactively.
|
|
14
|
+
|
|
15
|
+
- ✅ Drives the **real interactive TUI** of Claude & Codex over a PTY (node-pty)
|
|
16
|
+
- ✅ A **Decider** answers approvals/choices: `rule` (default), `command` (an LLM
|
|
17
|
+
CLI like `claude -p` / `codex exec`), `function`/API, or `always-approve`
|
|
18
|
+
- ✅ Both **Claude and Codex** are first-class and **verified end-to-end**
|
|
19
|
+
- ✅ Vendor-agnostic core: every agent is an `AgentAdapter`; nothing in `core/`
|
|
20
|
+
imports Claude/Codex
|
|
21
|
+
- ✅ Hard timeout, idle timeout, max-interactions, graceful cancel on every run
|
|
22
|
+
- ✅ Deterministic **fake interactive agent** makes the whole loop testable offline
|
|
23
|
+
- ✅ "Done" means **`pnpm typecheck` + `pnpm test` pass** — not "code was written"
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- Node.js **>= 18.19**
|
|
30
|
+
- [pnpm](https://pnpm.io/)
|
|
31
|
+
- [`node-pty`](https://www.npmjs.com/package/node-pty) — installed automatically;
|
|
32
|
+
it ships prebuilt binaries for macOS/Linux. agent-relay self-heals its
|
|
33
|
+
`spawn-helper` permissions at runtime, so it works across install methods.
|
|
34
|
+
- Optional, per adapter:
|
|
35
|
+
- [`claude`](https://www.npmjs.com/package/@anthropic-ai/claude-code) on PATH
|
|
36
|
+
- [`codex`](https://www.npmjs.com/package/@openai/codex) on PATH
|
|
37
|
+
|
|
38
|
+
Designed for **macOS / Linux** CLI use.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Global CLI (once published):
|
|
44
|
+
npm i -g agent-relay # or: pnpm add -g agent-relay
|
|
45
|
+
|
|
46
|
+
# From a local checkout:
|
|
47
|
+
pnpm install
|
|
48
|
+
pnpm build # tsup -> dist/cli.js + dist/index.js + .d.ts
|
|
49
|
+
npm i -g . # install this checkout globally
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Run from source without building: `pnpm dev -- <command>`.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
**Zero-config** — `init` is optional. With no `agent-relay.config.json`, the CLI
|
|
57
|
+
uses built-in defaults and creates its session/log dirs on first run, so you can
|
|
58
|
+
go straight to `npx`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx agent-relay run --adapter claude --prompt "현재 프로젝트의 타입 오류를 확인하고 수정해줘"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
agent-relay init # OPTIONAL: write a config you can edit
|
|
66
|
+
agent-relay adapters # list adapters
|
|
67
|
+
agent-relay doctor # check environment
|
|
68
|
+
|
|
69
|
+
# Deterministic, no real agent:
|
|
70
|
+
agent-relay run --adapter fake --prompt "test task"
|
|
71
|
+
|
|
72
|
+
# Real agents, driven interactively (the decider answers their prompts):
|
|
73
|
+
agent-relay run --adapter codex --prompt "현재 프로젝트의 타입 오류를 확인하고 수정해줘"
|
|
74
|
+
agent-relay run --adapter claude --prompt-file ./examples/task.md
|
|
75
|
+
|
|
76
|
+
agent-relay resume --session-id <sessionId>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Run `agent-relay init` only when you want a config file on disk to customize
|
|
80
|
+
(pick a different default adapter, point the decider at an LLM/local model, or
|
|
81
|
+
change the session/log dirs). Pass `--root /path/to/project` to run against
|
|
82
|
+
another project — the real `claude`/`codex` then loads *that* project's
|
|
83
|
+
`CLAUDE.md`, skills, MCP servers, and hooks natively.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## How it works
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
prompt ──> spawn agent in a PTY (its real TUI) ──> watch terminal output
|
|
91
|
+
│
|
|
92
|
+
output settles (idle) ▼
|
|
93
|
+
┌──> prompt detected? ──yes──> Decider.decide()
|
|
94
|
+
│ │
|
|
95
|
+
│ keystrokes <───────────────┘
|
|
96
|
+
│ (Enter / arrow+Enter / y/n / text)
|
|
97
|
+
│
|
|
98
|
+
└──> no prompt + sustained idle ──> quit cleanly (completed)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
1. The agent is launched **interactively** in a pseudo-terminal under **pure
|
|
102
|
+
autonomy** (the project's concept): Claude with `--dangerously-skip-permissions`,
|
|
103
|
+
Codex with `-s workspace-write -a never`. The agent rarely asks — but the
|
|
104
|
+
prompts that *still* appear (the directory-trust dialog, the occasional choice)
|
|
105
|
+
are what the Decider answers. `approvalPolicy: "gated"` makes the agent ask
|
|
106
|
+
more (Claude `--permission-mode acceptEdits`, Codex `-a on-request`);
|
|
107
|
+
`"readonly"` sandboxes it (Claude `--permission-mode plan`, Codex `-s read-only`).
|
|
108
|
+
2. When the terminal goes idle, agent-relay strips ANSI and **detects** a prompt:
|
|
109
|
+
a numbered/▶ **menu** (single choice), a **multi-select** menu (checkboxes
|
|
110
|
+
`[ ]`/`[x]`/`◯`/`◉`), a yes/no **approval**, or — opt-in — a free-text **input**
|
|
111
|
+
step. Multi-step flows (menu → menu → input → submit) are handled as a
|
|
112
|
+
sequence: each settled screen is detected, decided, answered, and the consumed
|
|
113
|
+
screen is dropped so the next step is read fresh.
|
|
114
|
+
3. The **Decider** decides; the keystrokes are written back into the PTY
|
|
115
|
+
(Enter to confirm a pre-selected option, arrow-down+Enter to pick another,
|
|
116
|
+
`y`/`n`, typed text, or — for a multi-select — a single batch that navigates
|
|
117
|
+
with ↑/↓, toggles the chosen rows with SPACE, and submits with `→`).
|
|
118
|
+
4. When the agent finishes and stays idle for `completionIdleMs` (default 8s,
|
|
119
|
+
tune with `--completion-idle-ms`), the session quits the TUI and is marked
|
|
120
|
+
**completed**. While the agent is *working* (its TUI shows a
|
|
121
|
+
"… esc to interrupt" indicator) the run is **never** treated as done — so a
|
|
122
|
+
multi-minute think/build is not cut short, however long it stays quiet.
|
|
123
|
+
|
|
124
|
+
> Driving a real TUI by scraping its output is inherently heuristic. The detector
|
|
125
|
+
> patterns and keymaps are tuned for the current Claude/Codex TUIs and are
|
|
126
|
+
> configurable per adapter; if a vendor changes its TUI, the patterns may need a
|
|
127
|
+
> tweak. Both agents are verified working today (see Testing).
|
|
128
|
+
|
|
129
|
+
**Multi-step menus** work out of the box for the real agents. A free-text
|
|
130
|
+
**input** step, however, is **opt-in**: the detector only recognizes one when you
|
|
131
|
+
give it an `inputPattern`, and it is **off by default for Claude/Codex** because
|
|
132
|
+
their idle main input box would otherwise be misread as "the agent is asking for
|
|
133
|
+
text". If you enable it, anchor the pattern to an unambiguous prompt phrase
|
|
134
|
+
(e.g. `/enter .*name:/i`, not a bare `/enter/i` that would also fire on
|
|
135
|
+
"Press Enter to continue"). Detection order within a screen is **choice →
|
|
136
|
+
approval → input**.
|
|
137
|
+
|
|
138
|
+
**Multi-select** (checkbox) menus are detected automatically when most rows carry
|
|
139
|
+
a `[ ]`/`[x]`/`◯`/`◉` box. The decider returns `optionIndexes` (the set that
|
|
140
|
+
should end up checked); the keymap then emits ONE batch — navigate ↑/↓, SPACE to
|
|
141
|
+
toggle only the rows that differ, then submit. The submit key defaults to `→`
|
|
142
|
+
(works for the common step-wizard "advance/Submit"); it is the `multiSelectSubmit`
|
|
143
|
+
arg of the keymap, so a TUI that submits with Enter-on-a-Next-row can be retuned.
|
|
144
|
+
The `rule` decider keeps the current selection and submits (no judgment); an LLM
|
|
145
|
+
decider picks the task-relevant rows.
|
|
146
|
+
|
|
147
|
+
## The Decider
|
|
148
|
+
|
|
149
|
+
The Decider answers every detected prompt. Choose it in config (`decider`) or
|
|
150
|
+
per run:
|
|
151
|
+
|
|
152
|
+
The guiding policy is **proceed by default, redirect only off-task danger**:
|
|
153
|
+
let the agent do its work (so the project actually progresses) and only refuse
|
|
154
|
+
an action that is *both* dangerous *and* unrelated to the task. Judging
|
|
155
|
+
"on-task vs off-task" needs the goal, so it is an **LLM** job — not a regex.
|
|
156
|
+
|
|
157
|
+
| Type | What it does |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `rule` (default) | Deterministic & offline, no judgment: **approves** every y/n and **confirms the recommended (first) option** on a menu so the task proceeds. (No label guessing by default — the TUI already pre-selects its affirmative choice.) Opt into `denyPatterns` (`rm -rf`, `sudo`, ...) to make it **label-aware**: deny dangerous approvals and steer menus to a negative option — see the caveat below. |
|
|
160
|
+
| `command` | Delegate to an **LLM CLI** — `claude -p`, `codex exec`, etc. The model gets the **task** and approves on-task work (even risky) while denying off-task danger with a redirect comment. |
|
|
161
|
+
| `api` | Same task-aware policy via an **OpenAI-compatible HTTP endpoint** (llama.cpp / vLLM / Ollama / LM Studio / OpenAI). Works with local open models. |
|
|
162
|
+
| `always-approve` | Approve/confirm everything. Maximum autonomy, no judgment. |
|
|
163
|
+
|
|
164
|
+
```jsonc
|
|
165
|
+
// agent-relay.config.json — pick ONE "decider":
|
|
166
|
+
|
|
167
|
+
// a) local open model (llama.cpp / vLLM / Ollama, OpenAI-compatible):
|
|
168
|
+
"decider": { "type": "api", "url": "http://localhost:9090/v1/chat/completions", "model": "default", "maxTokens": 1024 }
|
|
169
|
+
|
|
170
|
+
// b) Claude CLI as the decider:
|
|
171
|
+
"decider": { "type": "command", "command": "claude", "args": ["-p"] }
|
|
172
|
+
|
|
173
|
+
// c) Codex CLI as the decider (note: `codex exec` does NOT accept `-a`):
|
|
174
|
+
"decider": { "type": "command", "command": "codex", "args": ["exec", "--skip-git-repo-check", "-s", "read-only"] }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
…or **per run, with no config file** — the `--decider*` flags build the same
|
|
178
|
+
override on the fly (great for `npx`):
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# local open model (api):
|
|
182
|
+
agent-relay run -a claude -p "..." \
|
|
183
|
+
--decider api --decider-url http://localhost:9090/v1/chat/completions \
|
|
184
|
+
--decider-model default --decider-max-tokens 1024
|
|
185
|
+
|
|
186
|
+
# an LLM CLI (command) — pass the WHOLE command line as one quoted string:
|
|
187
|
+
agent-relay run -a claude -p "..." \
|
|
188
|
+
--decider command --decider-command "codex exec --skip-git-repo-check -s read-only"
|
|
189
|
+
|
|
190
|
+
# force a simple policy:
|
|
191
|
+
agent-relay run -a codex -p "..." --decider always-approve
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The type is inferred when obvious (`--decider-url` ⇒ `api`, `--decider-command`
|
|
195
|
+
⇒ `command`), so `--decider` itself is optional in those cases. A `--decider*`
|
|
196
|
+
flag **overrides** `config.decider` for that run; with no flag, the configured
|
|
197
|
+
(or default `rule`) decider is used. Complex command args with shell quoting
|
|
198
|
+
aren't supported on the flag — use a config file for those.
|
|
199
|
+
|
|
200
|
+
All four backends are verified. The task-aware policy is the important part:
|
|
201
|
+
given the task *"delete build/ and node_modules"*, an LLM decider **approves**
|
|
202
|
+
`rm -rf build node_modules` ("directly implements the task"); given the task
|
|
203
|
+
*"fix the README typo"*, it **denies** `rm -rf ~/Documents` ("dangerous and
|
|
204
|
+
off-task") with a redirect comment — the *same* command, opposite decisions.
|
|
205
|
+
The local model + rule deciders have also driven real Claude/Codex sessions
|
|
206
|
+
end-to-end. Embedders can plug any model via a `FunctionDecider`
|
|
207
|
+
(`async (request) => decision`) or the `onApprovalRequest` run-hook.
|
|
208
|
+
|
|
209
|
+
> A denial can carry a redirect `text`; agent-relay types it back to the agent
|
|
210
|
+
> when its TUI next asks "what should I do instead?" (best-effort, TUI-dependent).
|
|
211
|
+
|
|
212
|
+
> **Latency note:** the agent waits at its prompt while the decider thinks, so a
|
|
213
|
+
> slow decider (a large local model can take ~20 s/decision) is fine for menus
|
|
214
|
+
> the agent waits on, but raise `--idle-timeout-ms` for long sessions. The fast
|
|
215
|
+
> `rule` decider is the default for unattended runs.
|
|
216
|
+
|
|
217
|
+
> **Reasoning models:** the `api` decider's `maxTokens` (default **2048**) is a
|
|
218
|
+
> *cap*, not a target — a normal decision still costs only what it needs. But a
|
|
219
|
+
> reasoning model emits a long chain-of-thought before its JSON answer, so a
|
|
220
|
+
> too-small cap truncates it mid-thought (empty `content` → unparseable → safe
|
|
221
|
+
> deny). Keep `--decider-max-tokens` generous (≥1024) for such models; `content`
|
|
222
|
+
> is parsed first, with `reasoning_content` as a fallback.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Commands
|
|
227
|
+
|
|
228
|
+
- **`init`** — write `agent-relay.config.json` + `.agent-relay/{sessions,logs}`.
|
|
229
|
+
- **`adapters`** — list adapters (`claude`, `codex`, `fake`), mode, resume support.
|
|
230
|
+
- **`doctor`** — Node version, config validity, dirs, and whether `claude`/`codex`
|
|
231
|
+
are on PATH (missing CLIs are warnings, never errors).
|
|
232
|
+
- **`run`** — options: `-a/--adapter`, `-p/--prompt`, `-f/--prompt-file`,
|
|
233
|
+
`--cwd`, `--max-turns`, `--timeout-ms`, `--idle-timeout-ms`,
|
|
234
|
+
`--completion-idle-ms`, `--approval <auto|gated|readonly>`, `--ultracode`,
|
|
235
|
+
`--decider*`, `--verbose`, `--max-log-bytes`, `--dry-run`, `--extra <args...>`,
|
|
236
|
+
`--root`. Exit code `0` on `completed`, non-zero otherwise. (`--approval gated`
|
|
237
|
+
keeps the agent asking so the decider answers more; `auto` is pure-autonomy.
|
|
238
|
+
`--ultracode` (Claude) forces Opus and drives `/effort ultracode` in the TUI
|
|
239
|
+
before the task — the full preset, unattended; plain `--effort xhigh` is the
|
|
240
|
+
default otherwise.)
|
|
241
|
+
- **`sessions`** — list recorded sessions with log sizes + total. Prune with
|
|
242
|
+
`sessions --prune --keep <n>` / `--older-than <days>` / `--all` (deletes the
|
|
243
|
+
session JSON **and** its log). Logs accumulate forever otherwise.
|
|
244
|
+
- **`resume`** — continue a prior session with a follow-up prompt:
|
|
245
|
+
`resume --session-id <id> -p "..."`. **Both verified end-to-end** (recall prior
|
|
246
|
+
context + run the follow-up): **Claude** via `--continue`; **Codex** via its
|
|
247
|
+
captured NATIVE session id — `codex resume <id> "<prompt>"` (the codex adapter
|
|
248
|
+
records the rollout UUID from `~/.codex/sessions/…` after each run into the
|
|
249
|
+
session's `sessionRef`; a legacy session with no captured id falls back to
|
|
250
|
+
`resume --last`).
|
|
251
|
+
|
|
252
|
+
## Configuration
|
|
253
|
+
|
|
254
|
+
`agent-relay init` writes `agent-relay.config.json` (validated by a zod schema;
|
|
255
|
+
see [`agent-relay.config.example.json`](./agent-relay.config.example.json)):
|
|
256
|
+
|
|
257
|
+
```jsonc
|
|
258
|
+
{
|
|
259
|
+
"defaultAdapter": "claude",
|
|
260
|
+
"sessionsDir": ".agent-relay/sessions",
|
|
261
|
+
"logsDir": ".agent-relay/logs",
|
|
262
|
+
"defaults": {
|
|
263
|
+
"maxTurns": 20, // max interactions (prompts answered) before aborting
|
|
264
|
+
"timeoutMs": 1800000, // hard wall-clock cap
|
|
265
|
+
"idleTimeoutMs": 300000, // abort if no event for this long
|
|
266
|
+
"approvalPolicy": "auto", // "readonly" => sandbox the agent; otherwise it asks and the decider answers
|
|
267
|
+
"sandbox": "workspace-write"
|
|
268
|
+
},
|
|
269
|
+
"decider": { "type": "rule" },
|
|
270
|
+
"hooks": { "onComplete": "echo \"[$AGENT_RELAY_STATUS] $AGENT_RELAY_SESSION_ID\"" },
|
|
271
|
+
"adapters": {
|
|
272
|
+
"claude": { "type": "claude", "mode": "pty", "command": "claude", "args": [] },
|
|
273
|
+
"codex": { "type": "codex", "mode": "pty", "command": "codex", "args": [] },
|
|
274
|
+
"fake": { "type": "fake", "mode": "test" }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
- **`decider`** decides every prompt — this is the real approval mechanism (see
|
|
280
|
+
**The Decider**). `approvalPolicy` no longer "approves"; it only selects
|
|
281
|
+
`readonly` (Codex `-s read-only`, Claude `--permission-mode plan`) vs. letting
|
|
282
|
+
the agent ask normally.
|
|
283
|
+
- `adapters.<name>.args` are passed straight through to the underlying CLI
|
|
284
|
+
(after the adapter's own flags, before the prompt) — e.g.
|
|
285
|
+
`adapters.claude.args: ["--effort", "max"]` to set Claude's effort level
|
|
286
|
+
(`low|medium|high|xhigh|max`), or `["--model", "opus"]`. The same flags can be
|
|
287
|
+
set per-run with `--extra` (e.g. `run -a claude -p "..." --extra --effort max`).
|
|
288
|
+
`adapters.<name>.env` injects/overrides spawn env vars — the Claude adapter sets
|
|
289
|
+
`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` by default (override with
|
|
290
|
+
`adapters.claude.env: { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "0" }`).
|
|
291
|
+
- `defaults.completionIdleMs` (or `--completion-idle-ms`) tunes how long the
|
|
292
|
+
finished agent must stay quiet before its TUI is quit (default 8s).
|
|
293
|
+
- For a fully autonomous, no-prompt agent (no decider involvement), set
|
|
294
|
+
`adapters.claude.args: ["--dangerously-skip-permissions"]` /
|
|
295
|
+
`adapters.codex.args: ["--full-auto"]`.
|
|
296
|
+
- **`hooks`** are shell commands run at lifecycle points with `AGENT_RELAY_*`
|
|
297
|
+
env vars (`SESSION_ID`, `STATUS`, `ADAPTER`, `LOG_FILE`, `EXIT_CODE`, `CWD`).
|
|
298
|
+
|
|
299
|
+
> ⚠️ **Safety:** the default `rule` decider **approves everything** so the task
|
|
300
|
+
> can progress (danger-regex blocking is opt-in via `denyPatterns`, and is
|
|
301
|
+
> unreliable on mangled TUI text anyway). For real control, use `approvalPolicy:
|
|
302
|
+
> "readonly"` (sandbox), or an LLM `command`/`api` decider that approves on-task
|
|
303
|
+
> work and denies off-task danger. Don't point an auto-approving run at an
|
|
304
|
+
> untrusted task.
|
|
305
|
+
|
|
306
|
+
## Sessions, logs, completion
|
|
307
|
+
|
|
308
|
+
- `.agent-relay/sessions/<id>.json` — metadata (prompt, adapter, status, times,
|
|
309
|
+
logFile, exitCode, error, meta). Tiny; list/prune with `agent-relay sessions`.
|
|
310
|
+
- `.agent-relay/logs/<id>.log` — header + one line per event + footer. By default
|
|
311
|
+
the **raw stdout/stderr stream is omitted** (a TUI redraws constantly — it's
|
|
312
|
+
~98% noise), so a run logs only its meaningful events and stays ~1 KB. Use
|
|
313
|
+
`--verbose` for the full stream when debugging, and `--max-log-bytes` to cap it.
|
|
314
|
+
- Status lifecycle: `created → running → { completed | failed | timeout |
|
|
315
|
+
cancelled | waiting_approval }`. The `CompletionDetector` maps the run to a
|
|
316
|
+
terminal status (timeout/idle → `timeout`, cancel → `cancelled`, success/exit
|
|
317
|
+
0 → `completed`, else `failed`) and is extensible.
|
|
318
|
+
|
|
319
|
+
## Architecture
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
src/core/ types · config(zod) · session · logger · completion · decider ·
|
|
323
|
+
hooks · runner · errors · util/{ansi,line-splitter,which,...}
|
|
324
|
+
src/adapters/ registry · fake-adapter ·
|
|
325
|
+
interactive/ pty-session (the detect→decide→respond loop) · prompt-detector ·
|
|
326
|
+
interactive-adapter · claude-interactive · codex-interactive
|
|
327
|
+
src/commands/ init · adapters · doctor · run · resume
|
|
328
|
+
src/cli.ts commander CLI src/index.ts programmatic API
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Decoupling rule:** `core/` depends only on the `AgentAdapter` interface and
|
|
332
|
+
receives an adapter factory from `adapters/registry.ts`; it never imports a
|
|
333
|
+
vendor adapter. The `Decider` reaches adapters via the run context, so any
|
|
334
|
+
decision backend works with any agent.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { runAgent, createAdapterFactory, RuleDecider } from "agent-relay";
|
|
338
|
+
const outcome = await runAgent({
|
|
339
|
+
config, rootDir: process.cwd(), adapterName: "codex",
|
|
340
|
+
prompt: "Fix the failing test",
|
|
341
|
+
resolveAdapter: createAdapterFactory(),
|
|
342
|
+
decider: new RuleDecider(), // or a CommandDecider / FunctionDecider
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Testing
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
pnpm typecheck
|
|
350
|
+
pnpm test # 100+ offline, deterministic tests
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The default suite is fully offline. Its centerpiece is a **real PTY test**: a
|
|
354
|
+
deterministic fake interactive agent (`tests/fixtures/fake-interactive.cjs`) is
|
|
355
|
+
driven through `runPtySession` and the Decider answers its approval + menu
|
|
356
|
+
prompts — proving the detect → decide → respond → complete loop, plus deny and
|
|
357
|
+
abort paths. A second fixture (`fake-interactive-wizard.cjs`, `tests/wizard.test.ts`)
|
|
358
|
+
drives a **multi-step** flow — menu → menu → free-text input → `[y/n]` submit —
|
|
359
|
+
and asserts each step is detected on its own fresh screen (the per-step buffer
|
|
360
|
+
reset), that a non-sequentially-numbered menu maps to the right keystroke, and
|
|
361
|
+
that the submitted artifact reflects the exact navigated choices. It also covers
|
|
362
|
+
the decider, prompt detector, ANSI cleanup, config, sessions, hooks, completion,
|
|
363
|
+
safety rails, and the commands.
|
|
364
|
+
|
|
365
|
+
### Real Claude/Codex integration (opt-in)
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
AGENT_RELAY_RUN_REAL_AGENT_TESTS=1 pnpm test
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Skipped by default; each adapter self-skips when its CLI is absent; runs in a
|
|
372
|
+
temp dir with a trivial file-creation prompt. **Both Codex and Claude are
|
|
373
|
+
verified** end-to-end: agent-relay drives the real TUI, the rule decider answers
|
|
374
|
+
the trust/permission prompts, and the agent creates the file.
|
|
375
|
+
|
|
376
|
+
## Remaining TODO / future work
|
|
377
|
+
|
|
378
|
+
- **Structured-interactive backends**: Claude `--input-format stream-json` +
|
|
379
|
+
permission callbacks and Codex `app-server`/`mcp-server` would be more robust
|
|
380
|
+
than PTY scraping for those agents (the "structured-first" path). PTY is the
|
|
381
|
+
universal driver today.
|
|
382
|
+
- **Richer prompt detection / per-agent keymaps** as TUIs evolve.
|
|
383
|
+
- **Native session-id capture in PTY mode** — resume is wired via `--continue` /
|
|
384
|
+
`resume --last` (most-recent in cwd); capturing the real id would allow
|
|
385
|
+
resuming a *specific* older session.
|
|
386
|
+
- Add a `repository` URL to `package.json` before publishing.
|
|
387
|
+
|
|
388
|
+
## Working in this repo
|
|
389
|
+
|
|
390
|
+
See [`CLAUDE.md`](./CLAUDE.md). `.claude/settings.json` ships a Stop hook that
|
|
391
|
+
runs `pnpm typecheck` and blocks finishing on type errors.
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"defaultAdapter": "claude",
|
|
3
|
+
"sessionsDir": ".agent-relay/sessions",
|
|
4
|
+
"logsDir": ".agent-relay/logs",
|
|
5
|
+
"defaults": {
|
|
6
|
+
"maxTurns": 20,
|
|
7
|
+
"timeoutMs": 1800000,
|
|
8
|
+
"idleTimeoutMs": 300000,
|
|
9
|
+
"approvalPolicy": "auto",
|
|
10
|
+
"sandbox": "workspace-write",
|
|
11
|
+
"requireApprovalOnRiskyActions": false
|
|
12
|
+
},
|
|
13
|
+
"adapters": {
|
|
14
|
+
"claude": {
|
|
15
|
+
"type": "claude",
|
|
16
|
+
"mode": "pty",
|
|
17
|
+
"command": "claude",
|
|
18
|
+
"args": []
|
|
19
|
+
},
|
|
20
|
+
"codex": {
|
|
21
|
+
"type": "codex",
|
|
22
|
+
"mode": "pty",
|
|
23
|
+
"command": "codex",
|
|
24
|
+
"args": []
|
|
25
|
+
},
|
|
26
|
+
"fake": {
|
|
27
|
+
"type": "fake",
|
|
28
|
+
"mode": "test",
|
|
29
|
+
"args": []
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"decider": {
|
|
33
|
+
"type": "rule"
|
|
34
|
+
},
|
|
35
|
+
"hooks": {
|
|
36
|
+
"onComplete": "echo \"[$AGENT_RELAY_STATUS] $AGENT_RELAY_SESSION_ID\""
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|