agent-relay-runner 0.62.3 → 0.63.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/package.json +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/adapter.ts +2 -1
- package/src/attachment-cache.ts +4 -3
- package/src/busy-reconciler.ts +84 -0
- package/src/config.ts +143 -5
- package/src/index.ts +30 -13
- package/src/logger.ts +2 -1
- package/src/mcp-outbox.ts +86 -0
- package/src/outbox.ts +2 -1
- package/src/profile-home.ts +2 -1
- package/src/rate-limit.ts +2 -2
- package/src/runner-core.ts +2224 -0
- package/src/runner-helpers.ts +317 -0
- package/src/runner-insights.ts +106 -0
- package/src/runner.ts +14 -2660
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.1",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "runner"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"agent-relay-sdk": "0.2.
|
|
23
|
+
"agent-relay-sdk": "0.2.42"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/adapter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentProfile, Message } from "agent-relay-sdk";
|
|
2
2
|
import { isRecord } from "agent-relay-sdk";
|
|
3
3
|
import type { SessionEvent } from "./session-insights";
|
|
4
|
+
import { messageBodyMaxCharsFromEnv } from "./config";
|
|
4
5
|
|
|
5
6
|
export type SemanticStatus = "idle" | "busy" | "offline" | "error";
|
|
6
7
|
type ProviderWorkKind = "provider-turn" | "subagent";
|
|
@@ -202,7 +203,7 @@ export const DEFAULT_PROVIDER_MESSAGE_BODY_MAX_CHARS = 24_000;
|
|
|
202
203
|
// Resolve the delivered-body cap. Deployment-dependent (the right ceiling tracks the host's
|
|
203
204
|
// context budget), so it's overridable via env — set on the orchestrator/host that spawns runners.
|
|
204
205
|
export function providerMessageBodyMaxChars(): number {
|
|
205
|
-
const raw =
|
|
206
|
+
const raw = messageBodyMaxCharsFromEnv();
|
|
206
207
|
if (raw !== undefined) {
|
|
207
208
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
208
209
|
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
package/src/attachment-cache.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { basename, join } from "node:path";
|
|
|
4
4
|
import type { Artifact, Message } from "agent-relay-sdk";
|
|
5
5
|
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
6
6
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
7
|
+
import { attachmentCacheDirFromEnv } from "./config";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
10
|
|
|
@@ -34,11 +35,11 @@ function attachmentRefs(message: Message): Record<string, unknown>[] {
|
|
|
34
35
|
return refs.filter(isRecord);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function attachmentCacheRoot(agentId: string, rootDir =
|
|
38
|
+
function attachmentCacheRoot(agentId: string, rootDir = attachmentCacheDirFromEnv()): string {
|
|
38
39
|
return join(attachmentCacheBase(rootDir), safePathPart(agentId));
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function attachmentCacheBase(rootDir =
|
|
42
|
+
function attachmentCacheBase(rootDir = attachmentCacheDirFromEnv()): string {
|
|
42
43
|
return rootDir || join(homedir(), ".agent-relay", "attachments");
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -147,7 +148,7 @@ function withCacheError(ref: Record<string, unknown>, error: unknown): Record<st
|
|
|
147
148
|
|
|
148
149
|
export function pruneAttachmentCache(
|
|
149
150
|
agentId: string,
|
|
150
|
-
rootDir =
|
|
151
|
+
rootDir = attachmentCacheDirFromEnv(),
|
|
151
152
|
maxAgeMs = DEFAULT_CACHE_MAX_AGE_MS,
|
|
152
153
|
now = Date.now(),
|
|
153
154
|
): void {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
type ProviderActivity = "busy" | "idle" | "unknown";
|
|
2
|
+
|
|
3
|
+
const BUSY_RECONCILE_POLL_MS = 4_000;
|
|
4
|
+
const BUSY_RECONCILE_IDLE_CONFIRM = 8;
|
|
5
|
+
const BUSY_RECONCILE_IDLE_CONFIRM_NO_BUSY = 15;
|
|
6
|
+
const INTERRUPT_RECONCILE_DELAY_MS = 1_500;
|
|
7
|
+
|
|
8
|
+
interface BusyReconcilerDeps {
|
|
9
|
+
isStopped(): boolean;
|
|
10
|
+
hasProcess(): boolean;
|
|
11
|
+
currentStatus(): "idle" | "busy" | "offline" | "error";
|
|
12
|
+
isProviderBlocked(): boolean;
|
|
13
|
+
hasProviderTurn(): boolean;
|
|
14
|
+
canProbeActivity(): boolean;
|
|
15
|
+
probeActivity(): Promise<ProviderActivity> | undefined;
|
|
16
|
+
clearProviderTurn(reason: string): void;
|
|
17
|
+
sessionDebug(message: string): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class BusyReconciler {
|
|
21
|
+
private idleStreak = 0;
|
|
22
|
+
private timer?: ReturnType<typeof setInterval>;
|
|
23
|
+
private sawBusy = false;
|
|
24
|
+
|
|
25
|
+
constructor(private readonly deps: BusyReconcilerDeps) {}
|
|
26
|
+
|
|
27
|
+
arm(): void {
|
|
28
|
+
if (this.timer || !this.deps.canProbeActivity()) return;
|
|
29
|
+
this.idleStreak = 0;
|
|
30
|
+
this.sawBusy = false;
|
|
31
|
+
this.timer = setInterval(() => { void this.run(); }, BUSY_RECONCILE_POLL_MS);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
disarm(): void {
|
|
35
|
+
if (this.timer) clearInterval(this.timer);
|
|
36
|
+
this.timer = undefined;
|
|
37
|
+
this.idleStreak = 0;
|
|
38
|
+
this.sawBusy = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
scheduleInterruptReconcile(): void {
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
if (this.deps.isStopped() || !this.deps.hasProcess()) return;
|
|
44
|
+
void (async () => {
|
|
45
|
+
if (this.deps.currentStatus() !== "busy" || this.deps.isProviderBlocked()) return;
|
|
46
|
+
const probe = this.deps.probeActivity();
|
|
47
|
+
let activity: ProviderActivity = "unknown";
|
|
48
|
+
try { if (probe) activity = await probe; } catch { return; }
|
|
49
|
+
this.deps.sessionDebug(`post-interrupt reconcile probe=${activity}`);
|
|
50
|
+
if (activity === "idle") this.deps.clearProviderTurn("post-interrupt");
|
|
51
|
+
})();
|
|
52
|
+
}, INTERRUPT_RECONCILE_DELAY_MS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async run(): Promise<void> {
|
|
56
|
+
const probe = this.deps.probeActivity();
|
|
57
|
+
if (this.deps.isStopped() || !this.deps.hasProcess() || !probe) {
|
|
58
|
+
this.disarm();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (this.deps.currentStatus() !== "busy" || this.deps.isProviderBlocked()) {
|
|
62
|
+
this.idleStreak = 0;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!this.deps.hasProviderTurn()) {
|
|
66
|
+
this.disarm();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
let activity: ProviderActivity;
|
|
70
|
+
try { activity = await probe; } catch { return; }
|
|
71
|
+
if (activity === "busy") this.sawBusy = true;
|
|
72
|
+
if (activity !== "idle") {
|
|
73
|
+
this.idleStreak = 0;
|
|
74
|
+
this.deps.sessionDebug(`reconcile probe=${activity} sawBusy=${this.sawBusy} streak=${this.idleStreak}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.idleStreak += 1;
|
|
78
|
+
const confirm = this.sawBusy ? BUSY_RECONCILE_IDLE_CONFIRM : BUSY_RECONCILE_IDLE_CONFIRM_NO_BUSY;
|
|
79
|
+
this.deps.sessionDebug(`reconcile probe=idle sawBusy=${this.sawBusy} streak=${this.idleStreak}/${confirm}`);
|
|
80
|
+
if (this.idleStreak < confirm) return;
|
|
81
|
+
this.disarm();
|
|
82
|
+
this.deps.clearProviderTurn(this.sawBusy ? "backstop reconciler" : "backstop reconciler (no-busy-observed)");
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, hostname } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { DEFAULT_RELAY_URL, RELAY_TOKEN_HEADER, errMessage, stringValue } from "agent-relay-sdk";
|
|
6
6
|
import type { SettingEntry, WorkspaceMetadata } from "agent-relay-sdk";
|
|
7
7
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
interface ProviderConfig {
|
|
10
|
+
command: string;
|
|
11
|
+
defaultArgs: string[];
|
|
12
|
+
env: Record<string, string>;
|
|
13
|
+
pluginDirs: string[];
|
|
14
|
+
defaultCapabilities: string[];
|
|
15
|
+
defaultApprovalMode: string;
|
|
16
|
+
defaultTags: string[];
|
|
17
|
+
chatCaptureMode: "final" | "full";
|
|
18
|
+
reasoningCapture?: boolean;
|
|
19
|
+
headless: {
|
|
20
|
+
tmuxPrefix: string;
|
|
21
|
+
shutdownTimeoutMs: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
9
24
|
|
|
10
25
|
interface GlobalRunnerConfig {
|
|
11
26
|
relayUrl: string;
|
|
@@ -17,7 +32,7 @@ interface LoadedProviderConfig extends ProviderConfig {
|
|
|
17
32
|
path: string;
|
|
18
33
|
}
|
|
19
34
|
|
|
20
|
-
function agentRelayHome(): string {
|
|
35
|
+
export function agentRelayHome(): string {
|
|
21
36
|
return process.env.AGENT_RELAY_HOME || join(homedir(), ".agent-relay");
|
|
22
37
|
}
|
|
23
38
|
|
|
@@ -48,12 +63,135 @@ export function loadGlobalConfig(home = agentRelayHome()): GlobalRunnerConfig {
|
|
|
48
63
|
const path = join(home, "config.json");
|
|
49
64
|
const parsed = readJson(path);
|
|
50
65
|
return {
|
|
51
|
-
relayUrl: stringValue(parsed.relayUrl) ??
|
|
52
|
-
token: stringValue(parsed.token) ??
|
|
66
|
+
relayUrl: stringValue(parsed.relayUrl) ?? relayUrlFromEnv() ?? DEFAULT_RELAY_URL,
|
|
67
|
+
token: stringValue(parsed.token) ?? relayTokenFromEnv(),
|
|
53
68
|
defaultCwd: stringValue(parsed.defaultCwd) ?? process.cwd(),
|
|
54
69
|
};
|
|
55
70
|
}
|
|
56
71
|
|
|
72
|
+
function relayUrlFromEnv(): string | undefined {
|
|
73
|
+
return process.env.AGENT_RELAY_URL;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function relayTokenFromEnv(): string | undefined {
|
|
77
|
+
return process.env.AGENT_RELAY_TOKEN;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function runtimeTokenProfileFromEnv(): string | undefined {
|
|
81
|
+
return process.env.AGENT_RELAY_TOKEN_PROFILE;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function runtimeTokenJtiFromEnv(): string | undefined {
|
|
85
|
+
return process.env.AGENT_RELAY_TOKEN_JTI;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function runtimeTokenExpiresAtFromEnv(): string | undefined {
|
|
89
|
+
return process.env.AGENT_RELAY_TOKEN_EXPIRES_AT;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function agentProfileNameFromEnv(): string | undefined {
|
|
93
|
+
return process.env.AGENT_RELAY_AGENT_PROFILE;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function agentProfileJsonFromEnv(): string | undefined {
|
|
97
|
+
return process.env.AGENT_RELAY_AGENT_PROFILE_JSON;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function workspaceJsonFromEnv(): string | undefined {
|
|
101
|
+
return process.env.AGENT_RELAY_WORKSPACE_JSON;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function tmuxSessionFromEnv(): string | undefined {
|
|
105
|
+
return process.env.AGENT_RELAY_TMUX_SESSION;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function policyNameFromEnv(): string | undefined {
|
|
109
|
+
return process.env.AGENT_RELAY_POLICY;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function spawnRequestIdFromEnv(): string | undefined {
|
|
113
|
+
return process.env.AGENT_RELAY_SPAWN_REQUEST_ID;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function automationIdFromEnv(): string | undefined {
|
|
117
|
+
return process.env.AGENT_RELAY_AUTOMATION_ID;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function automationRunIdFromEnv(): string | undefined {
|
|
121
|
+
return process.env.AGENT_RELAY_AUTOMATION_RUN_ID;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function lifecycleFromEnv(): string | undefined {
|
|
125
|
+
return process.env.AGENT_RELAY_LIFECYCLE;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function sessionDebugEnabled(): boolean {
|
|
129
|
+
return process.env.AGENT_RELAY_SESSION_DEBUG === "1";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function logLevelFromEnv(): string | undefined {
|
|
133
|
+
return process.env.AGENT_RELAY_LOG_LEVEL;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function mcpProxyEnabledFromEnv(): boolean {
|
|
137
|
+
return !["0", "false", "off"].includes((process.env.AGENT_RELAY_MCP_PROXY ?? "").trim().toLowerCase());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function runnerInfoFileFromEnv(): string | undefined {
|
|
141
|
+
return process.env.AGENT_RELAY_RUNNER_INFO_FILE;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function runnerOutboxDirFromEnv(): string | undefined {
|
|
145
|
+
return process.env.AGENT_RELAY_RUNNER_OUTBOX_DIR;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function runnerOutboxDirWithInfoFallback(): string | undefined {
|
|
149
|
+
const dir = runnerOutboxDirFromEnv();
|
|
150
|
+
if (dir) return dir;
|
|
151
|
+
const infoFile = runnerInfoFileFromEnv();
|
|
152
|
+
return infoFile ? join(dirname(infoFile), "outbox") : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function capsFromEnv(): string | undefined {
|
|
156
|
+
return process.env.AGENT_RELAY_CAPS;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function tagsFromEnv(): string | undefined {
|
|
160
|
+
return process.env.AGENT_RELAY_TAGS;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function workspaceModeFromEnv(): string | undefined {
|
|
164
|
+
return process.env.AGENT_RELAY_WORKSPACE_MODE;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function orchestratorBaseDirFromEnv(): string | undefined {
|
|
168
|
+
return process.env.AGENT_RELAY_ORCHESTRATOR_BASE_DIR;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function runnerLogFileFromEnv(): string | undefined {
|
|
172
|
+
return typeof process.env.AGENT_RELAY_LOG_FILE === "string" ? process.env.AGENT_RELAY_LOG_FILE : undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function orchestratorUrlFromEnv(): string | undefined {
|
|
176
|
+
return process.env.AGENT_RELAY_ORCHESTRATOR_URL;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function contextStateDirFromEnv(): string | undefined {
|
|
180
|
+
return process.env.AGENT_RELAY_CONTEXT_STATE_DIR;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function attachmentCacheDirFromEnv(): string | undefined {
|
|
184
|
+
return process.env.AGENT_RELAY_ATTACHMENT_CACHE_DIR;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function providerHomeRootFromEnv(): string {
|
|
188
|
+
return process.env.AGENT_RELAY_PROVIDER_HOME_ROOT || join(homedir(), ".agent-relay", "provider-homes");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function messageBodyMaxCharsFromEnv(): string | undefined {
|
|
192
|
+
return process.env.AGENT_RELAY_MESSAGE_BODY_MAX_CHARS;
|
|
193
|
+
}
|
|
194
|
+
|
|
57
195
|
export function loadProviderConfig(provider: string, home = agentRelayHome()): LoadedProviderConfig {
|
|
58
196
|
const path = join(providersDir(home), `${provider}.json`);
|
|
59
197
|
const raw = readJson(path);
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,24 @@ import { hostname } from "node:os";
|
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { isatty } from "node:tty";
|
|
5
5
|
import { AgentRunner } from "./runner";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
agentProfileJsonFromEnv,
|
|
8
|
+
agentProfileNameFromEnv,
|
|
9
|
+
automationIdFromEnv,
|
|
10
|
+
automationRunIdFromEnv,
|
|
11
|
+
lifecycleFromEnv,
|
|
12
|
+
loadGlobalConfig,
|
|
13
|
+
loadProviderConfigFromRelay,
|
|
14
|
+
policyNameFromEnv,
|
|
15
|
+
resolveCwd,
|
|
16
|
+
runnerId,
|
|
17
|
+
runtimeTokenExpiresAtFromEnv,
|
|
18
|
+
runtimeTokenJtiFromEnv,
|
|
19
|
+
runtimeTokenProfileFromEnv,
|
|
20
|
+
spawnRequestIdFromEnv,
|
|
21
|
+
tmuxSessionFromEnv,
|
|
22
|
+
workspaceJsonFromEnv,
|
|
23
|
+
} from "./config";
|
|
7
24
|
import { createProviderAdapter } from "./providers";
|
|
8
25
|
import { VERSION } from "./version";
|
|
9
26
|
import type { AgentProfile, SpawnProvider, WorkspaceMetadata } from "agent-relay-sdk";
|
|
@@ -49,9 +66,9 @@ export async function main(argv = process.argv): Promise<void> {
|
|
|
49
66
|
interactive: opts.interactive,
|
|
50
67
|
relayUrl,
|
|
51
68
|
token: opts.token ?? globalConfig.token,
|
|
52
|
-
runtimeTokenProfile:
|
|
53
|
-
runtimeTokenJti:
|
|
54
|
-
runtimeTokenExpiresAt: parseTokenExpiresAt(
|
|
69
|
+
runtimeTokenProfile: runtimeTokenProfileFromEnv(),
|
|
70
|
+
runtimeTokenJti: runtimeTokenJtiFromEnv(),
|
|
71
|
+
runtimeTokenExpiresAt: parseTokenExpiresAt(runtimeTokenExpiresAtFromEnv()),
|
|
55
72
|
provider: opts.provider,
|
|
56
73
|
cwd,
|
|
57
74
|
runnerId: id,
|
|
@@ -84,20 +101,20 @@ export async function main(argv = process.argv): Promise<void> {
|
|
|
84
101
|
approvalMode,
|
|
85
102
|
label: opts.label,
|
|
86
103
|
rig: opts.rig,
|
|
87
|
-
profile: opts.profile ??
|
|
88
|
-
agentProfile: parseAgentProfileEnv(
|
|
89
|
-
workspace: parseWorkspaceEnv(
|
|
104
|
+
profile: opts.profile ?? agentProfileNameFromEnv(),
|
|
105
|
+
agentProfile: parseAgentProfileEnv(agentProfileJsonFromEnv()),
|
|
106
|
+
workspace: parseWorkspaceEnv(workspaceJsonFromEnv()),
|
|
90
107
|
prompt: opts.prompt,
|
|
91
108
|
systemPromptAppend: opts.systemPromptAppend,
|
|
92
|
-
tmuxSession:
|
|
109
|
+
tmuxSession: tmuxSessionFromEnv(),
|
|
93
110
|
tags: opts.tags,
|
|
94
111
|
capabilities: opts.caps,
|
|
95
112
|
providerArgs: opts.providerArgs,
|
|
96
|
-
policyName:
|
|
97
|
-
spawnRequestId:
|
|
98
|
-
automationId:
|
|
99
|
-
automationRunId:
|
|
100
|
-
lifecycle: normalizeAgentLifecycle(
|
|
113
|
+
policyName: policyNameFromEnv(),
|
|
114
|
+
spawnRequestId: spawnRequestIdFromEnv(),
|
|
115
|
+
automationId: automationIdFromEnv(),
|
|
116
|
+
automationRunId: automationRunIdFromEnv(),
|
|
117
|
+
lifecycle: normalizeAgentLifecycle(lifecycleFromEnv()) ?? "persistent",
|
|
101
118
|
startedAt: Date.now(),
|
|
102
119
|
providerConfig,
|
|
103
120
|
adapter,
|
package/src/logger.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
4
|
+
import { logLevelFromEnv } from "./config";
|
|
4
5
|
|
|
5
6
|
// Phase 1 observability (#198): one leveled, runtime-togglable logger for the
|
|
6
7
|
// Runner and the provider adapters below it. Replaces the ad-hoc scatter of
|
|
@@ -47,7 +48,7 @@ export class Logger {
|
|
|
47
48
|
private logDir: string;
|
|
48
49
|
|
|
49
50
|
constructor(config: LoggerConfig = {}) {
|
|
50
|
-
this.level = config.level ?? parseLogLevel(
|
|
51
|
+
this.level = config.level ?? parseLogLevel(logLevelFromEnv()) ?? "info";
|
|
51
52
|
this.agentId = config.agentId ?? "runner";
|
|
52
53
|
this.headless = config.headless ?? false;
|
|
53
54
|
this.logDir = config.logDir ?? join(process.env.HOME || ".", ".agent-relay", "logs");
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { SendMessageInput } from "agent-relay-sdk";
|
|
2
|
+
import type { RelayHttpClient } from "agent-relay-sdk";
|
|
3
|
+
import { relayMcpEndpoint } from "./relay-mcp";
|
|
4
|
+
import type { OutboxRecord } from "./outbox";
|
|
5
|
+
import { deliverContinuationArchiveRecord } from "./continuation-archive";
|
|
6
|
+
import { logger } from "./logger";
|
|
7
|
+
import { isHttpAuthError, isHttpStatusError } from "./runner-helpers";
|
|
8
|
+
|
|
9
|
+
interface RunnerOutboxDelivery {
|
|
10
|
+
record: OutboxRecord;
|
|
11
|
+
http: RelayHttpClient;
|
|
12
|
+
relayUrl: string;
|
|
13
|
+
token?: string;
|
|
14
|
+
updatePayload(seq: number, payload: unknown): void;
|
|
15
|
+
sessionLog(message: string): void;
|
|
16
|
+
recoverRuntimeTokenAfterAuthFailure(source: string): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function deliverRunnerOutboxEvent(input: RunnerOutboxDelivery): Promise<void> {
|
|
20
|
+
const { record, http } = input;
|
|
21
|
+
try {
|
|
22
|
+
if (record.kind === "session-message") {
|
|
23
|
+
await http.sendMessage({
|
|
24
|
+
...(record.payload as SendMessageInput),
|
|
25
|
+
occurredAt: record.occurredAt,
|
|
26
|
+
idempotencyKey: record.idempotencyKey,
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (record.kind === "insight") {
|
|
31
|
+
await http.recordInsightObservation({
|
|
32
|
+
...(record.payload as Parameters<RelayHttpClient["recordInsightObservation"]>[0]),
|
|
33
|
+
occurredAt: record.occurredAt,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (record.kind === "continuation-archive") {
|
|
38
|
+
await deliverContinuationArchiveRecord({
|
|
39
|
+
record,
|
|
40
|
+
http,
|
|
41
|
+
updatePayload: input.updatePayload,
|
|
42
|
+
sessionLog: input.sessionLog,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (record.kind === "mcp-tool-call") {
|
|
47
|
+
await deliverBufferedMcpCall(input);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
logger.warn("outbox", `dropping event with unknown kind: ${record.kind}`);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isHttpStatusError(error, 409)) return;
|
|
53
|
+
if (isHttpAuthError(error)) input.recoverRuntimeTokenAfterAuthFailure("outbox");
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function deliverBufferedMcpCall(input: {
|
|
59
|
+
record: OutboxRecord;
|
|
60
|
+
relayUrl: string;
|
|
61
|
+
token?: string;
|
|
62
|
+
recoverRuntimeTokenAfterAuthFailure(source: string): void;
|
|
63
|
+
}): Promise<void> {
|
|
64
|
+
const payload = input.record.payload as { tool: string; arguments: Record<string, unknown> };
|
|
65
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
66
|
+
if (input.token) headers.authorization = `Bearer ${input.token}`;
|
|
67
|
+
const response = await fetch(relayMcpEndpoint(input.relayUrl), {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers,
|
|
70
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: payload.tool, arguments: payload.arguments } }),
|
|
71
|
+
});
|
|
72
|
+
if (response.status === 401 || response.status === 403) {
|
|
73
|
+
input.recoverRuntimeTokenAfterAuthFailure("mcp-outbox");
|
|
74
|
+
throw new Error(`relay rejected buffered ${payload.tool} with ${response.status}`);
|
|
75
|
+
}
|
|
76
|
+
if (response.status >= 500) throw new Error(`relay ${response.status} on buffered ${payload.tool}`);
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const body = await response.text().catch(() => "");
|
|
79
|
+
logger.warn("mcp-outbox", `buffered ${payload.tool} permanently rejected (${response.status}); dropping: ${body.slice(0, 200)}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const json = await response.json().catch(() => null) as { error?: { message?: string } } | null;
|
|
83
|
+
if (json?.error) {
|
|
84
|
+
logger.warn("mcp-outbox", `buffered ${payload.tool} returned a tool error; dropping: ${json.error.message ?? "(no detail)"}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/outbox.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
6
6
|
import { errMessage } from "agent-relay-sdk";
|
|
7
7
|
import { logger } from "./logger";
|
|
8
|
+
import { runnerOutboxDirFromEnv } from "./config";
|
|
8
9
|
|
|
9
10
|
// Phase 2 (#196) — the "nothing is ever lost" half. Runner→server events that used to be
|
|
10
11
|
// fire-and-forget over HTTP (session turns, reasoning/tool traces, prompt echoes, insights,
|
|
@@ -112,7 +113,7 @@ export class Outbox {
|
|
|
112
113
|
this.maxBackoffMs = options.maxBackoffMs ?? DEFAULTS.maxBackoffMs;
|
|
113
114
|
this.pollMs = options.pollMs ?? DEFAULTS.pollMs;
|
|
114
115
|
|
|
115
|
-
const dir = options.dir ??
|
|
116
|
+
const dir = options.dir ?? runnerOutboxDirFromEnv() ?? join(tmpdir(), "agent-relay-outbox");
|
|
116
117
|
this.path = options.dir === ":memory:" ? ":memory:" : join(dir, `outbox-${safeName(this.agentId)}.sqlite`);
|
|
117
118
|
if (this.path !== ":memory:") mkdirSync(dirname(this.path), { recursive: true });
|
|
118
119
|
|
package/src/profile-home.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
5
5
|
import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
|
|
6
6
|
import { CLAUDE_RELAY_MANUAL } from "./relay-instructions";
|
|
7
|
+
import { providerHomeRootFromEnv } from "./config";
|
|
7
8
|
|
|
8
9
|
type ProviderHome = {
|
|
9
10
|
path: string;
|
|
@@ -105,7 +106,7 @@ function readHostClaudeConfig(): Record<string, unknown> | undefined {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
function providerHomePath(provider: "claude" | "codex", config: RunnerSpawnConfig): string {
|
|
108
|
-
const root =
|
|
109
|
+
const root = providerHomeRootFromEnv();
|
|
109
110
|
const profileName = sanitizePathPart(config.agentProfile?.name || config.profile || "profile");
|
|
110
111
|
const instance = sanitizePathPart(config.instanceId || config.runnerId);
|
|
111
112
|
return join(root, provider, profileName, instance);
|
package/src/rate-limit.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const RATE_LIMIT_BLOCK_REASON = "rate_limit";
|
|
|
14
14
|
// fall back to the resume sweep's poll window.
|
|
15
15
|
const MAX_RESET_AHEAD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
interface RateLimitHoldInput {
|
|
18
18
|
errorType?: string;
|
|
19
19
|
/** Unix ms the limit window resets, when known. */
|
|
20
20
|
resetAt?: number;
|
|
@@ -58,7 +58,7 @@ const LIMIT_PHRASE_RE = /(?:hit (?:your|the)\s*(?:[\w-]+\s+)*limit|usage limit|\
|
|
|
58
58
|
const RESET_AT_RE = /\bresets?\b(?:\s+at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*(?:\(([^)]+)\))?/i;
|
|
59
59
|
const TRY_AGAIN_RE = /\btry again in\s+(\d+)\s*(second|minute|hour|day)s?/i;
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
interface PaneRateLimit {
|
|
62
62
|
/** The matched limit line, for the chat notice / observability. */
|
|
63
63
|
message: string;
|
|
64
64
|
/** Unix ms reset, when parseable from the pane (else undefined → resume falls back to poll). */
|