agent-relay-runner 0.10.6 → 0.10.8
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/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +24 -0
- package/plugins/claude/skills/pair/SKILL.md +26 -0
- package/plugins/claude/skills/reply/SKILL.md +25 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +24 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +24 -0
- package/plugins/codex/skills/pair/SKILL.md +25 -0
- package/plugins/codex/skills/reply/SKILL.md +24 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +24 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/src/adapter.ts +1 -0
- package/src/adapters/claude.ts +125 -0
- package/src/adapters/codex.ts +55 -4
- package/src/config.ts +1 -1
- package/src/index.ts +40 -15
- package/src/runner.ts +70 -14
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: disconnect
|
|
3
|
+
description: End the current Agent Relay pair session. Use when the user invokes /disconnect or asks to hang up, unpair, or disconnect from a paired agent.
|
|
4
|
+
argument-hint: "[PAIR_ID]"
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Relay Disconnect
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-relay /disconnect $ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If no pair id is supplied, the CLI ends the active pair for this session. Report the result briefly.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: label
|
|
3
|
+
description: Read, set, or clear the current Agent Relay agent label. Use when the user invokes /label or asks to rename this relay agent.
|
|
4
|
+
argument-hint: "[LABEL|--clear]"
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Relay Label
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-relay /label $ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
agent-relay /label backend-fixer
|
|
20
|
+
agent-relay /label --clear
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Report the new label or current label briefly.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: message
|
|
3
|
+
description: Send a normal Agent Relay message to another agent, label, tag, capability, or broadcast target. Use when the user invokes /message or asks to send a one-off relay message.
|
|
4
|
+
argument-hint: "<target> <message> [--subject TEXT] [--channel NAME]"
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Relay Message
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-relay /message $ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
agent-relay /message codex "Can you look at that failing action?"
|
|
20
|
+
agent-relay /message tag:backend "Does anyone see the regression?"
|
|
21
|
+
agent-relay /message broadcast "Standup in five minutes"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Report the sent message id briefly.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pair
|
|
3
|
+
description: Start, inspect, accept, reject, or send messages in an Agent Relay two-agent pair session. Use when the user invokes /pair or asks to pair this agent with Codex, Claude, or another relay agent.
|
|
4
|
+
argument-hint: "<target|status|accept|reject|send> [args]"
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Relay Pair
|
|
9
|
+
|
|
10
|
+
Run the Agent Relay CLI with the user's arguments:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-relay /pair $ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Use this for pair-session commands such as:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
agent-relay /pair codex "Debug flaky tests"
|
|
20
|
+
agent-relay /pair status
|
|
21
|
+
agent-relay /pair accept PAIR_ID
|
|
22
|
+
agent-relay /pair reject PAIR_ID
|
|
23
|
+
agent-relay /pair send PAIR_ID "What do you see?"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Report the command output briefly. If the CLI cannot detect this session's agent id, rerun with `--agent AGENT_ID` or `--from AGENT_ID` using the Agent Relay ID shown in session context.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reply
|
|
3
|
+
description: Reply to an Agent Relay message by ID. Auto-routes to the sender and inherits channel context — no target needed. Use when the user invokes /reply or asks to reply to a specific relay message.
|
|
4
|
+
argument-hint: "<messageId> <message>"
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Relay Reply
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-relay /reply $ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The server auto-routes the reply to the original sender and inherits the channel (Telegram, Slack, etc.) if applicable. No target or channel ID needed.
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
agent-relay /reply 206 "Sounds good, I'll take a look"
|
|
22
|
+
agent-relay /reply 42 "Done — the fix is in commit abc123"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Report the sent message id and resolved target briefly.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: send-claimable
|
|
3
|
+
description: Send a claimable Agent Relay work item so one matching agent can claim and handle it. Use when the user invokes /send-claimable or wants to enqueue work for another agent.
|
|
4
|
+
argument-hint: "<target> <message> [--subject TEXT] [--channel NAME]"
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Relay Send Claimable
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-relay /send-claimable $ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
agent-relay /send-claimable codex "Claim this and inspect the failing action"
|
|
20
|
+
agent-relay /send-claimable tag:backend "Fix the failing API test"
|
|
21
|
+
agent-relay /send-claimable cap:review "Review the migration patch"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Report the sent claimable message id briefly.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: status
|
|
3
|
+
description: Show Agent Relay status for this session, including relay health, current agent id, label, tags, readiness, and active pair state. Use when the user invokes /status or asks for relay connection status.
|
|
4
|
+
argument-hint: "[--json]"
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Relay Status
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-relay /status $ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Summarize the current relay connection, agent identity, label, tags, and active pair state.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tags
|
|
3
|
+
description: List or update Agent Relay tags for the current session. Use when the user invokes /tags or asks to set, add, remove, or inspect relay tags.
|
|
4
|
+
argument-hint: "[TAG ...|--list|--add TAGS|--remove TAGS]"
|
|
5
|
+
allowed-tools: [Bash]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Relay Tags
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-relay /tags $ARGUMENTS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
agent-relay /tags
|
|
20
|
+
agent-relay /tags backend tests urgent
|
|
21
|
+
agent-relay /tags --add backend,tests
|
|
22
|
+
agent-relay /tags --remove urgent
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Report the resulting tags briefly.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: disconnect
|
|
3
|
+
description: End the current Agent Relay pair session. Use when the user invokes /disconnect or asks to hang up, unpair, or disconnect from a paired agent.
|
|
4
|
+
argument-hint: "[PAIR_ID]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Relay Disconnect
|
|
8
|
+
|
|
9
|
+
Run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agent-relay /disconnect $ARGUMENTS
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
If no pair id is supplied, the CLI ends the active pair for this session. Report the result briefly.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: label
|
|
3
|
+
description: Read, set, or clear the current Agent Relay agent label. Use when the user invokes /label or asks to rename this relay agent.
|
|
4
|
+
argument-hint: "[LABEL|--clear]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Relay Label
|
|
8
|
+
|
|
9
|
+
Run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agent-relay /label $ARGUMENTS
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
agent-relay /label backend-fixer
|
|
19
|
+
agent-relay /label --clear
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Report the new label or current label briefly.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: message
|
|
3
|
+
description: Send a normal Agent Relay message to another agent, label, tag, capability, policy, or broadcast target. Use when the user invokes /message or asks to send a one-off relay message.
|
|
4
|
+
argument-hint: "<target> <message> [--subject TEXT] [--channel NAME]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Relay Message
|
|
8
|
+
|
|
9
|
+
Run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agent-relay /message $ARGUMENTS
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
agent-relay /message codex "Can you look at that failing action?"
|
|
19
|
+
agent-relay /message tag:backend "Does anyone see the regression?"
|
|
20
|
+
agent-relay /message policy:reviewer "Review this when you come online"
|
|
21
|
+
agent-relay /message broadcast "Standup in five minutes"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Report the sent message id briefly.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pair
|
|
3
|
+
description: Start, inspect, accept, reject, or send messages in an Agent Relay two-agent pair session. Use when the user invokes /pair or asks to pair this agent with Codex, Claude, or another relay agent.
|
|
4
|
+
argument-hint: "<target|status|accept|reject|send> [args]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Relay Pair
|
|
8
|
+
|
|
9
|
+
Run the Agent Relay CLI with the user's arguments:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agent-relay /pair $ARGUMENTS
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Use this for pair-session commands such as:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
agent-relay /pair codex "Debug flaky tests"
|
|
19
|
+
agent-relay /pair status
|
|
20
|
+
agent-relay /pair accept PAIR_ID
|
|
21
|
+
agent-relay /pair reject PAIR_ID
|
|
22
|
+
agent-relay /pair send PAIR_ID "What do you see?"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Report the command output briefly. If the CLI cannot detect this session's agent id, rerun with `--agent AGENT_ID` or `--from AGENT_ID` using the Agent Relay ID shown in session context.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reply
|
|
3
|
+
description: Reply to an Agent Relay message by ID. Auto-routes to the sender and inherits channel context, so no target is needed. Use when the user invokes /reply or asks to reply to a specific relay message.
|
|
4
|
+
argument-hint: "<messageId> <message>"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Relay Reply
|
|
8
|
+
|
|
9
|
+
Run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agent-relay /reply $ARGUMENTS
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The server auto-routes the reply to the original sender and inherits the channel if applicable. No target or channel ID is needed.
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
agent-relay /reply 206 "Sounds good, I'll take a look"
|
|
21
|
+
agent-relay /reply 42 "Done; the fix is in commit abc123"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Report the sent message id and resolved target briefly.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: send-claimable
|
|
3
|
+
description: Send a claimable Agent Relay work item so one matching agent can claim and handle it. Use when the user invokes /send-claimable or wants to enqueue work for another agent.
|
|
4
|
+
argument-hint: "<target> <message> [--subject TEXT] [--channel NAME]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Relay Send Claimable
|
|
8
|
+
|
|
9
|
+
Run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agent-relay /send-claimable $ARGUMENTS
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
agent-relay /send-claimable codex "Claim this and inspect the failing action"
|
|
19
|
+
agent-relay /send-claimable tag:backend "Fix the failing API test"
|
|
20
|
+
agent-relay /send-claimable cap:review "Review the migration patch"
|
|
21
|
+
agent-relay /send-claimable policy:reviewer "Review this when the managed reviewer starts"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Report the sent claimable message id briefly.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: status
|
|
3
|
+
description: Show Agent Relay status for this session, including relay health, current agent id, label, tags, readiness, and active pair state. Use when the user invokes /status or asks for relay connection status.
|
|
4
|
+
argument-hint: "[--json]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Relay Status
|
|
8
|
+
|
|
9
|
+
Run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agent-relay /status $ARGUMENTS
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Summarize the current relay connection, agent identity, label, tags, and active pair state.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tags
|
|
3
|
+
description: List or update Agent Relay tags for the current session. Use when the user invokes /tags or asks to set, add, remove, or inspect relay tags.
|
|
4
|
+
argument-hint: "[TAG ...|--list|--add TAGS|--remove TAGS]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Relay Tags
|
|
8
|
+
|
|
9
|
+
Run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
agent-relay /tags $ARGUMENTS
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
agent-relay /tags
|
|
19
|
+
agent-relay /tags backend tests urgent
|
|
20
|
+
agent-relay /tags --add backend,tests
|
|
21
|
+
agent-relay /tags --remove urgent
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Report the resulting tags briefly.
|
package/src/adapter.ts
CHANGED
package/src/adapters/claude.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ManagedProcess, ProviderAdapter, ProviderConfig, RunnerSpawnConfig
|
|
|
5
5
|
export class ClaudeAdapter implements ProviderAdapter {
|
|
6
6
|
readonly provider = "claude";
|
|
7
7
|
private statusCb: (status: SemanticStatus) => void = () => {};
|
|
8
|
+
private tmuxWatcher?: Timer;
|
|
8
9
|
|
|
9
10
|
onStatusChange(cb: (status: SemanticStatus) => void): void {
|
|
10
11
|
this.statusCb = cb;
|
|
@@ -12,6 +13,11 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
12
13
|
|
|
13
14
|
async spawn(config: RunnerSpawnConfig): Promise<ManagedProcess> {
|
|
14
15
|
const args = this.buildSpawnArgs(config, config.providerConfig as ProviderConfig);
|
|
16
|
+
|
|
17
|
+
if (config.headless) {
|
|
18
|
+
return this.spawnHeadless(config, args);
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
const proc = Bun.spawn([args.command, ...args.args], {
|
|
16
22
|
cwd: args.cwd,
|
|
17
23
|
env: args.env,
|
|
@@ -24,6 +30,11 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
async shutdown(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
|
|
33
|
+
const tmuxSession = process.meta?.tmuxSession as string | undefined;
|
|
34
|
+
if (tmuxSession) {
|
|
35
|
+
await this.shutdownTmux(tmuxSession, opts);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
27
38
|
await terminateProcess(process, opts);
|
|
28
39
|
}
|
|
29
40
|
|
|
@@ -36,7 +47,10 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
36
47
|
buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs {
|
|
37
48
|
const pluginRoot = resolve(import.meta.dir, "../../plugins/claude");
|
|
38
49
|
const pluginDirs = [...new Set([pluginRoot, ...providerConfig.pluginDirs])];
|
|
50
|
+
const isClaudeRig = /claude-rig/.test(providerConfig.command);
|
|
51
|
+
const rigPrefix = isClaudeRig && config.rig ? ["launch", config.rig] : [];
|
|
39
52
|
const args = [
|
|
53
|
+
...rigPrefix,
|
|
40
54
|
...pluginDirs.flatMap((dir) => ["--plugin-dir", dir]),
|
|
41
55
|
...providerConfig.defaultArgs,
|
|
42
56
|
...config.providerArgs,
|
|
@@ -52,6 +66,117 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
52
66
|
},
|
|
53
67
|
};
|
|
54
68
|
}
|
|
69
|
+
|
|
70
|
+
buildTmuxArgs(config: RunnerSpawnConfig, spawnArgs: SpawnArgs): { sessionName: string; args: string[] } {
|
|
71
|
+
const sessionName = tmuxSessionName(config.providerConfig.headless.tmuxPrefix, config.instanceId, config.label);
|
|
72
|
+
const shellCmd = [spawnArgs.command, ...spawnArgs.args].map(shellQuote).join(" ");
|
|
73
|
+
const tmuxArgs = ["new-session", "-d", "-s", sessionName, "-x", "200", "-y", "50"];
|
|
74
|
+
|
|
75
|
+
const envKeys = tmuxEnvKeys(spawnArgs.env, config.providerConfig.env);
|
|
76
|
+
for (const key of envKeys) {
|
|
77
|
+
if (spawnArgs.env[key] !== undefined) {
|
|
78
|
+
tmuxArgs.push("-e", `${key}=${spawnArgs.env[key]}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
tmuxArgs.push("-c", spawnArgs.cwd, shellCmd);
|
|
83
|
+
return { sessionName, args: tmuxArgs };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async spawnHeadless(config: RunnerSpawnConfig, spawnArgs: SpawnArgs): Promise<ManagedProcess> {
|
|
87
|
+
const { sessionName, args: tmuxArgs } = this.buildTmuxArgs(config, spawnArgs);
|
|
88
|
+
|
|
89
|
+
Bun.spawnSync(["tmux", "kill-session", "-t", sessionName], {
|
|
90
|
+
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const result = Bun.spawnSync(["tmux", ...tmuxArgs], {
|
|
94
|
+
stdin: "ignore",
|
|
95
|
+
stdout: "pipe",
|
|
96
|
+
stderr: "pipe",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (result.exitCode !== 0) {
|
|
100
|
+
const stderr = result.stderr.toString().trim();
|
|
101
|
+
throw new Error(`tmux session creation failed: ${stderr || `exit code ${result.exitCode}`}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.watchTmuxSession(sessionName);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
pid: undefined,
|
|
108
|
+
process: undefined,
|
|
109
|
+
meta: {
|
|
110
|
+
monitor: config.monitor,
|
|
111
|
+
tmuxSession: sessionName,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private watchTmuxSession(sessionName: string): void {
|
|
117
|
+
if (this.tmuxWatcher) clearInterval(this.tmuxWatcher);
|
|
118
|
+
this.tmuxWatcher = setInterval(() => {
|
|
119
|
+
if (!tmuxHasSession(sessionName)) {
|
|
120
|
+
clearInterval(this.tmuxWatcher!);
|
|
121
|
+
this.tmuxWatcher = undefined;
|
|
122
|
+
this.statusCb("offline");
|
|
123
|
+
}
|
|
124
|
+
}, 2000);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async shutdownTmux(sessionName: string, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
|
|
128
|
+
if (this.tmuxWatcher) {
|
|
129
|
+
clearInterval(this.tmuxWatcher);
|
|
130
|
+
this.tmuxWatcher = undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (opts.graceful && tmuxHasSession(sessionName)) {
|
|
134
|
+
Bun.spawnSync(["tmux", "send-keys", "-t", sessionName, "C-c"], {
|
|
135
|
+
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
139
|
+
while (Date.now() < deadline) {
|
|
140
|
+
if (!tmuxHasSession(sessionName)) return;
|
|
141
|
+
await Bun.sleep(500);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Bun.spawnSync(["tmux", "kill-session", "-t", sessionName], {
|
|
146
|
+
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function tmuxSessionName(prefix: string, instanceId: string, label?: string): string {
|
|
152
|
+
if (label) return `${prefix}-${label.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase()}`;
|
|
153
|
+
return `${prefix}-${instanceId.slice(0, 8)}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function tmuxHasSession(sessionName: string): boolean {
|
|
157
|
+
const result = Bun.spawnSync(["tmux", "has-session", "-t", sessionName], {
|
|
158
|
+
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
159
|
+
});
|
|
160
|
+
return result.exitCode === 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function tmuxEnvKeys(env: Record<string, string>, providerEnv: Record<string, string>): string[] {
|
|
164
|
+
const keys = new Set<string>();
|
|
165
|
+
for (const key of Object.keys(env)) {
|
|
166
|
+
if (key.startsWith("AGENT_RELAY_") || key.startsWith("CLAUDE_")) {
|
|
167
|
+
keys.add(key);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const key of Object.keys(providerEnv)) {
|
|
171
|
+
keys.add(key);
|
|
172
|
+
}
|
|
173
|
+
return [...keys].sort();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function shellQuote(arg: string): string {
|
|
177
|
+
if (arg.length === 0) return "''";
|
|
178
|
+
if (/^[a-zA-Z0-9_./:@=+,-]+$/.test(arg)) return arg;
|
|
179
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
55
180
|
}
|
|
56
181
|
|
|
57
182
|
async function terminateProcess(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
|
package/src/adapters/codex.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
1
3
|
import type { Message } from "agent-relay-sdk";
|
|
2
4
|
import { providerMessageText, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type RunnerSpawnConfig, type SemanticStatus, type SpawnArgs } from "../adapter";
|
|
3
5
|
import { CodexAppClient, type ClientEvent } from "./codex-client";
|
|
@@ -27,8 +29,8 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
27
29
|
client.onEvent((event) => this.handleCodexEvent(event));
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
const tui = !config.headless && config.providerConfig.command
|
|
31
|
-
? Bun.spawn([
|
|
32
|
+
const tui = !config.headless && isCodexCliCommand(config.providerConfig.command)
|
|
33
|
+
? Bun.spawn([config.providerConfig.command, "--remote", appServerUrl, ...bundledSkillConfigArgs(), ...config.providerConfig.defaultArgs, ...config.providerArgs], {
|
|
32
34
|
cwd: config.cwd,
|
|
33
35
|
env: args.env,
|
|
34
36
|
stdin: "inherit",
|
|
@@ -62,8 +64,14 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
62
64
|
|
|
63
65
|
buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs {
|
|
64
66
|
const appServerUrl = String(config.appServerUrl || process.env.CODEX_APP_SERVER_URL || `ws://127.0.0.1:${config.controlPort + 1000}`);
|
|
65
|
-
const args = providerConfig.command
|
|
66
|
-
? [
|
|
67
|
+
const args = isCodexCliCommand(providerConfig.command)
|
|
68
|
+
? [
|
|
69
|
+
"app-server",
|
|
70
|
+
...codexAppServerConfigArgs(providerConfig.defaultArgs, config.providerArgs),
|
|
71
|
+
...bundledSkillConfigArgs(),
|
|
72
|
+
"--listen",
|
|
73
|
+
appServerUrl,
|
|
74
|
+
]
|
|
67
75
|
: [...providerConfig.defaultArgs, ...config.providerArgs];
|
|
68
76
|
return {
|
|
69
77
|
command: providerConfig.command,
|
|
@@ -90,6 +98,49 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
100
|
|
|
101
|
+
export function isCodexCliCommand(command: string): boolean {
|
|
102
|
+
return basename(command) === "codex";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function bundledCodexSkillDirs(baseDir = resolve(import.meta.dir, "../../plugins/codex/skills")): string[] {
|
|
106
|
+
if (!existsSync(baseDir)) return [];
|
|
107
|
+
return readdirSync(baseDir, { withFileTypes: true })
|
|
108
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(baseDir, entry.name, "SKILL.md")))
|
|
109
|
+
.map((entry) => join(baseDir, entry.name))
|
|
110
|
+
.sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function bundledSkillConfigArgs(skillDirs = bundledCodexSkillDirs()): string[] {
|
|
114
|
+
if (skillDirs.length === 0) return [];
|
|
115
|
+
const skillsConfig = skillDirs
|
|
116
|
+
.map((dir) => `{path=${tomlString(dir)},enabled=true}`)
|
|
117
|
+
.join(",");
|
|
118
|
+
return ["-c", `skills.config=[${skillsConfig}]`];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function codexAppServerConfigArgs(...argLists: string[][]): string[] {
|
|
122
|
+
const result: string[] = [];
|
|
123
|
+
for (const args of argLists) {
|
|
124
|
+
for (let i = 0; i < args.length; i++) {
|
|
125
|
+
const arg = args[i];
|
|
126
|
+
if (arg === "-c" || arg === "--config" || arg === "--enable" || arg === "--disable") {
|
|
127
|
+
const value = args[i + 1];
|
|
128
|
+
if (value !== undefined) {
|
|
129
|
+
result.push(arg, value);
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
} else if (arg?.startsWith("--config=") || arg?.startsWith("--enable=") || arg?.startsWith("--disable=")) {
|
|
133
|
+
result.push(arg);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function tomlString(value: string): string {
|
|
141
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
142
|
+
}
|
|
143
|
+
|
|
93
144
|
async function connectWithRetry(client: CodexAppClient, attempts = 40): Promise<void> {
|
|
94
145
|
let lastError: unknown;
|
|
95
146
|
for (let i = 0; i < attempts; i++) {
|
package/src/config.ts
CHANGED
|
@@ -24,7 +24,7 @@ function providersDir(home = agentRelayHome()): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function defaultProviderConfig(provider: string): ProviderConfig {
|
|
27
|
-
const command = provider === "claude" ? "claude
|
|
27
|
+
const command = provider === "claude" ? "claude" : provider;
|
|
28
28
|
return {
|
|
29
29
|
command,
|
|
30
30
|
defaultArgs: provider === "claude" ? ["--dangerously-skip-permissions"] : [],
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { basename } from "node:path";
|
|
3
|
+
import { isatty } from "node:tty";
|
|
3
4
|
import { ClaudeAdapter } from "./adapters/claude";
|
|
4
5
|
import { CodexAdapter } from "./adapters/codex";
|
|
5
6
|
import { AgentRunner } from "./runner";
|
|
@@ -8,13 +9,17 @@ import { loadGlobalConfig, loadProviderConfig, resolveCwd, runnerId } from "./co
|
|
|
8
9
|
interface CliOptions {
|
|
9
10
|
provider: "claude" | "codex";
|
|
10
11
|
headless: boolean;
|
|
12
|
+
interactive: boolean;
|
|
11
13
|
cwd?: string;
|
|
14
|
+
rig?: string;
|
|
12
15
|
relayUrl?: string;
|
|
13
16
|
token?: string;
|
|
14
17
|
approvalMode?: string;
|
|
15
18
|
label?: string;
|
|
16
19
|
agentId?: string;
|
|
17
20
|
prompt?: string;
|
|
21
|
+
tags: string[];
|
|
22
|
+
caps: string[];
|
|
18
23
|
providerArgs: string[];
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -25,6 +30,10 @@ export async function main(argv = process.argv): Promise<void> {
|
|
|
25
30
|
const cwd = resolveCwd(opts.cwd, globalConfig.defaultCwd);
|
|
26
31
|
const id = runnerId(opts.provider, cwd, opts.label);
|
|
27
32
|
const adapter = opts.provider === "claude" ? new ClaudeAdapter() : new CodexAdapter();
|
|
33
|
+
|
|
34
|
+
let exitResolve: (code: number) => void;
|
|
35
|
+
const exitPromise = new Promise<number>((resolve) => { exitResolve = resolve; });
|
|
36
|
+
|
|
28
37
|
const runner = new AgentRunner({
|
|
29
38
|
provider: opts.provider,
|
|
30
39
|
runnerId: id,
|
|
@@ -36,17 +45,28 @@ export async function main(argv = process.argv): Promise<void> {
|
|
|
36
45
|
headless: opts.headless,
|
|
37
46
|
approvalMode: opts.approvalMode ?? providerConfig.defaultApprovalMode,
|
|
38
47
|
label: opts.label,
|
|
48
|
+
rig: opts.rig,
|
|
39
49
|
prompt: opts.prompt,
|
|
50
|
+
tags: opts.tags,
|
|
51
|
+
capabilities: opts.caps,
|
|
40
52
|
providerArgs: opts.providerArgs,
|
|
53
|
+
policyName: process.env.AGENT_RELAY_POLICY,
|
|
54
|
+
spawnRequestId: process.env.AGENT_RELAY_SPAWN_REQUEST_ID,
|
|
55
|
+
tmuxSession: process.env.AGENT_RELAY_TMUX_SESSION,
|
|
56
|
+
startedAt: Date.now(),
|
|
41
57
|
providerConfig,
|
|
42
58
|
adapter,
|
|
59
|
+
onProviderExit: (code) => exitResolve(code ?? 1),
|
|
43
60
|
});
|
|
44
61
|
await runner.run();
|
|
45
|
-
|
|
62
|
+
|
|
63
|
+
const code = await Promise.race([exitPromise, signalPromise()]);
|
|
64
|
+
await runner.stop();
|
|
65
|
+
process.exit(code);
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
function parseArgs(argv: string[]): CliOptions {
|
|
49
|
-
const bin =
|
|
69
|
+
const bin = [argv[1], process.env._].map((s) => basename(s || "")).join(" ");
|
|
50
70
|
let provider: "claude" | "codex" = bin.includes("claude") ? "claude" : "codex";
|
|
51
71
|
const relayArgs = argv.slice(2);
|
|
52
72
|
const providerSep = relayArgs.indexOf("--");
|
|
@@ -54,24 +74,30 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
54
74
|
const providerArgs = providerSep >= 0 ? relayArgs.slice(providerSep + 1) : [];
|
|
55
75
|
let headless = false;
|
|
56
76
|
let cwd: string | undefined;
|
|
77
|
+
let rig: string | undefined;
|
|
57
78
|
let relayUrl: string | undefined;
|
|
58
79
|
let token: string | undefined;
|
|
59
80
|
let approvalMode: string | undefined;
|
|
60
81
|
let label: string | undefined;
|
|
61
82
|
let agentId: string | undefined;
|
|
62
83
|
let prompt: string | undefined;
|
|
84
|
+
let tags: string[] = [];
|
|
85
|
+
let caps: string[] = [];
|
|
63
86
|
|
|
64
87
|
for (let i = 0; i < ownArgs.length; i++) {
|
|
65
88
|
const arg = ownArgs[i];
|
|
66
89
|
if (arg === "claude" || arg === "codex") provider = arg;
|
|
67
90
|
else if (arg === "--headless") headless = true;
|
|
68
91
|
else if (arg === "--cwd" && ownArgs[i + 1]) cwd = ownArgs[++i];
|
|
92
|
+
else if (arg === "--rig" && ownArgs[i + 1]) rig = ownArgs[++i];
|
|
69
93
|
else if (arg === "--relay-url" && ownArgs[i + 1]) relayUrl = ownArgs[++i];
|
|
70
94
|
else if (arg === "--token" && ownArgs[i + 1]) token = ownArgs[++i];
|
|
71
95
|
else if ((arg === "--approval" || arg === "--approval-mode") && ownArgs[i + 1]) approvalMode = ownArgs[++i];
|
|
72
96
|
else if (arg === "--label" && ownArgs[i + 1]) label = ownArgs[++i];
|
|
73
97
|
else if (arg === "--agent-id" && ownArgs[i + 1]) agentId = ownArgs[++i];
|
|
74
98
|
else if (arg === "--prompt" && ownArgs[i + 1]) prompt = ownArgs[++i];
|
|
99
|
+
else if (arg === "--tags" && ownArgs[i + 1]) tags = csvSplit(ownArgs[++i]!);
|
|
100
|
+
else if (arg === "--caps" && ownArgs[i + 1]) caps = csvSplit(ownArgs[++i]!);
|
|
75
101
|
else if (arg === "--help" || arg === "-h") {
|
|
76
102
|
printHelp(provider);
|
|
77
103
|
process.exit(0);
|
|
@@ -80,24 +106,23 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
80
106
|
}
|
|
81
107
|
}
|
|
82
108
|
|
|
83
|
-
|
|
109
|
+
const interactive = !headless && isatty(0);
|
|
110
|
+
return { provider, headless, interactive, cwd, rig, relayUrl, token, approvalMode, label, agentId, prompt, tags, caps, providerArgs };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function csvSplit(value: string): string[] {
|
|
114
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
84
115
|
}
|
|
85
116
|
|
|
86
117
|
function printHelp(provider: string): void {
|
|
87
|
-
console.log(`${provider}-relay [--headless] [--cwd PATH] [--relay-url URL] [--approval MODE] [--label NAME] [--agent-id ID] [-- provider-args...]`);
|
|
118
|
+
console.log(`${provider}-relay [--headless] [--rig NAME] [--cwd PATH] [--relay-url URL] [--approval MODE] [--label NAME] [--agent-id ID] [--tags a,b] [--caps x,y] [-- provider-args...]`);
|
|
88
119
|
}
|
|
89
120
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
await stop();
|
|
96
|
-
process.exit(0);
|
|
97
|
-
};
|
|
98
|
-
process.on("SIGINT", () => void shutdown());
|
|
99
|
-
process.on("SIGTERM", () => void shutdown());
|
|
100
|
-
await new Promise(() => {});
|
|
121
|
+
function signalPromise(): Promise<number> {
|
|
122
|
+
return new Promise<number>((resolve) => {
|
|
123
|
+
process.on("SIGINT", () => resolve(130));
|
|
124
|
+
process.on("SIGTERM", () => resolve(143));
|
|
125
|
+
});
|
|
101
126
|
}
|
|
102
127
|
|
|
103
128
|
if (import.meta.main) {
|
package/src/runner.ts
CHANGED
|
@@ -16,10 +16,18 @@ interface RunnerOptions {
|
|
|
16
16
|
headless: boolean;
|
|
17
17
|
approvalMode: string;
|
|
18
18
|
label?: string;
|
|
19
|
+
rig?: string;
|
|
19
20
|
prompt?: string;
|
|
21
|
+
tags: string[];
|
|
22
|
+
capabilities: string[];
|
|
20
23
|
providerArgs: string[];
|
|
24
|
+
policyName?: string;
|
|
25
|
+
spawnRequestId?: string;
|
|
26
|
+
tmuxSession?: string;
|
|
27
|
+
startedAt: number;
|
|
21
28
|
providerConfig: ProviderConfig;
|
|
22
29
|
adapter: ProviderAdapter;
|
|
30
|
+
onProviderExit?: (code: number | null) => void;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export class AgentRunner {
|
|
@@ -30,6 +38,7 @@ export class AgentRunner {
|
|
|
30
38
|
private control?: ControlServer;
|
|
31
39
|
private process?: ManagedProcess;
|
|
32
40
|
private stopped = false;
|
|
41
|
+
private exitCommandInProgress = false;
|
|
33
42
|
private delivering = false;
|
|
34
43
|
private readonly pendingMessages = new Map<number, Message>();
|
|
35
44
|
|
|
@@ -47,6 +56,8 @@ export class AgentRunner {
|
|
|
47
56
|
capabilities: [
|
|
48
57
|
...new Set([
|
|
49
58
|
...options.providerConfig.defaultCapabilities,
|
|
59
|
+
...options.capabilities,
|
|
60
|
+
...csvTags(process.env.AGENT_RELAY_CAPS),
|
|
50
61
|
"lifecycle.shutdown.hard",
|
|
51
62
|
"lifecycle.restart.hard",
|
|
52
63
|
"lifecycle.status.semantic",
|
|
@@ -56,10 +67,14 @@ export class AgentRunner {
|
|
|
56
67
|
"capabilities.report",
|
|
57
68
|
]),
|
|
58
69
|
],
|
|
59
|
-
tags: [...new Set([options.provider, ...csvTags(process.env.AGENT_RELAY_TAGS), ...options.providerConfig.defaultTags, ...(options.headless ? ["headless"] : [])])],
|
|
70
|
+
tags: [...new Set([options.provider, ...csvTags(process.env.AGENT_RELAY_TAGS), ...options.tags, ...options.providerConfig.defaultTags, ...(options.headless ? ["headless"] : [])])],
|
|
60
71
|
meta: {
|
|
61
72
|
provider: options.provider,
|
|
62
73
|
runnerId: options.runnerId,
|
|
74
|
+
startedAt: options.startedAt,
|
|
75
|
+
tmuxSession: options.tmuxSession ?? null,
|
|
76
|
+
policyName: options.policyName ?? null,
|
|
77
|
+
spawnRequestId: options.spawnRequestId ?? null,
|
|
63
78
|
runnerManaged: true,
|
|
64
79
|
cwd: options.cwd,
|
|
65
80
|
approvalMode: options.approvalMode,
|
|
@@ -77,7 +92,10 @@ export class AgentRunner {
|
|
|
77
92
|
|
|
78
93
|
async run(): Promise<void> {
|
|
79
94
|
this.control = startControlServer({ onStatus: (status) => this.setProviderStatus(status) });
|
|
80
|
-
this.options.adapter.onStatusChange((status) =>
|
|
95
|
+
this.options.adapter.onStatusChange((status) => {
|
|
96
|
+
this.setProviderStatus(status);
|
|
97
|
+
if (runnerShouldResolveProviderExit(status, this.exitCommandInProgress)) this.options.onProviderExit?.(status === "offline" ? 0 : 1);
|
|
98
|
+
});
|
|
81
99
|
this.bus.on("message.new", (message) => this.enqueueMessage(message as Message));
|
|
82
100
|
this.bus.on("command", (type, params, commandId, command) => {
|
|
83
101
|
void this.handleCommand(type, params, commandId, command);
|
|
@@ -123,6 +141,7 @@ export class AgentRunner {
|
|
|
123
141
|
headless: this.options.headless,
|
|
124
142
|
approvalMode: this.options.approvalMode,
|
|
125
143
|
...(this.options.label ? { label: this.options.label } : {}),
|
|
144
|
+
...(this.options.rig ? { rig: this.options.rig } : {}),
|
|
126
145
|
...(this.options.prompt ? { prompt: this.options.prompt } : {}),
|
|
127
146
|
providerArgs: this.options.providerArgs,
|
|
128
147
|
providerConfig: this.options.providerConfig,
|
|
@@ -173,6 +192,7 @@ export class AgentRunner {
|
|
|
173
192
|
if (type !== "agent.shutdown" && type !== "agent.restart" && type !== "agent.kill") return;
|
|
174
193
|
|
|
175
194
|
const exitAfterCommand = type !== "agent.restart";
|
|
195
|
+
if (exitAfterCommand) this.exitCommandInProgress = true;
|
|
176
196
|
this.claims.startClaim("command", commandId);
|
|
177
197
|
this.publishStatus();
|
|
178
198
|
await this.updateCommand(commandId, "accepted");
|
|
@@ -180,7 +200,14 @@ export class AgentRunner {
|
|
|
180
200
|
try {
|
|
181
201
|
if (type === "agent.restart") await this.restartProvider();
|
|
182
202
|
else await this.shutdownProvider(type === "agent.kill");
|
|
183
|
-
await this.updateCommand(commandId, "succeeded", {
|
|
203
|
+
await this.updateCommand(commandId, "succeeded", {
|
|
204
|
+
action: type,
|
|
205
|
+
agentId: this.agentId,
|
|
206
|
+
runnerId: this.options.runnerId,
|
|
207
|
+
policyName: this.options.policyName,
|
|
208
|
+
spawnRequestId: this.options.spawnRequestId,
|
|
209
|
+
reason: typeof params.reason === "string" ? params.reason : undefined,
|
|
210
|
+
});
|
|
184
211
|
} catch (error) {
|
|
185
212
|
await this.updateCommand(commandId, "failed", undefined, error instanceof Error ? error.message : String(error)).catch(() => {});
|
|
186
213
|
} finally {
|
|
@@ -251,6 +278,10 @@ export class AgentRunner {
|
|
|
251
278
|
ready: agentStatus !== "offline" && !this.stopped,
|
|
252
279
|
meta: {
|
|
253
280
|
runnerId: this.options.runnerId,
|
|
281
|
+
startedAt: this.options.startedAt,
|
|
282
|
+
tmuxSession: this.options.tmuxSession ?? null,
|
|
283
|
+
policyName: this.options.policyName ?? null,
|
|
284
|
+
spawnRequestId: this.options.spawnRequestId ?? null,
|
|
254
285
|
...(status === "error" ? { terminalStatus: "error" } : {}),
|
|
255
286
|
busyReasons: this.claims.reasons(),
|
|
256
287
|
transport: this.bus.transportState,
|
|
@@ -259,20 +290,41 @@ export class AgentRunner {
|
|
|
259
290
|
}
|
|
260
291
|
|
|
261
292
|
private matchesMessage(message: Message): boolean {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
return false;
|
|
293
|
+
return runnerMessageMatches(message, {
|
|
294
|
+
agentId: this.agentId,
|
|
295
|
+
label: this.options.label,
|
|
296
|
+
provider: this.options.provider,
|
|
297
|
+
tags: this.options.tags,
|
|
298
|
+
capabilities: this.options.capabilities,
|
|
299
|
+
defaultTags: this.options.providerConfig.defaultTags,
|
|
300
|
+
defaultCapabilities: this.options.providerConfig.defaultCapabilities,
|
|
301
|
+
});
|
|
273
302
|
}
|
|
274
303
|
}
|
|
275
304
|
|
|
305
|
+
export function runnerMessageMatches(message: Pick<Message, "to" | "resolvedToAgent">, target: {
|
|
306
|
+
agentId: string;
|
|
307
|
+
label?: string;
|
|
308
|
+
provider: string;
|
|
309
|
+
tags: string[];
|
|
310
|
+
capabilities: string[];
|
|
311
|
+
defaultTags: string[];
|
|
312
|
+
defaultCapabilities: string[];
|
|
313
|
+
}): boolean {
|
|
314
|
+
if (message.resolvedToAgent === target.agentId) return true;
|
|
315
|
+
if (message.to === target.agentId || message.to === "broadcast") return true;
|
|
316
|
+
if (target.label && message.to === `label:${target.label}`) return true;
|
|
317
|
+
if (message.to.startsWith("tag:")) {
|
|
318
|
+
const tag = message.to.slice("tag:".length);
|
|
319
|
+
return target.tags.includes(tag) || target.defaultTags.includes(tag) || tag === target.provider;
|
|
320
|
+
}
|
|
321
|
+
if (message.to.startsWith("cap:")) {
|
|
322
|
+
const cap = message.to.slice("cap:".length);
|
|
323
|
+
return target.capabilities.includes(cap) || target.defaultCapabilities.includes(cap);
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
276
328
|
function csvTags(raw: string | undefined): string[] {
|
|
277
329
|
return (raw || "").split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
278
330
|
}
|
|
@@ -294,6 +346,10 @@ export function runnerBusErrorAction(code: string, stopped: boolean): "ignore" |
|
|
|
294
346
|
return "log";
|
|
295
347
|
}
|
|
296
348
|
|
|
349
|
+
export function runnerShouldResolveProviderExit(status: SemanticStatus, exitCommandInProgress: boolean): boolean {
|
|
350
|
+
return !exitCommandInProgress && (status === "offline" || status === "error");
|
|
351
|
+
}
|
|
352
|
+
|
|
297
353
|
function lifecycleCapabilities(): Record<string, true> {
|
|
298
354
|
return {
|
|
299
355
|
shutdownHard: true,
|