agent-relay-runner 0.78.6 → 0.78.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 +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/adapters/codex-agent-message-capture.ts +43 -7
- package/src/adapters/codex.ts +1 -1
- package/src/claude-quota-harvest.ts +57 -0
- package/src/context-probe-state.ts +6 -0
- package/src/process-meta.ts +9 -0
- package/src/quota.ts +3 -0
- package/src/runner-core.ts +17 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.78.
|
|
3
|
+
"version": "0.78.8",
|
|
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.58"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
type EmitAgentMessage = (body: string) => void;
|
|
2
|
+
type ResponseEntry = { itemId?: string; body: string };
|
|
3
|
+
type CompleteItemOptions = { includeInResponse?: boolean; phase?: unknown };
|
|
2
4
|
|
|
3
5
|
export class CodexAgentMessageCapture {
|
|
4
|
-
private segments:
|
|
5
|
-
private currentSegment:
|
|
6
|
+
private segments: ResponseEntry[][] = [];
|
|
7
|
+
private currentSegment: ResponseEntry[] = [];
|
|
6
8
|
private readonly buffers = new Map<string, string>();
|
|
7
9
|
private readonly flushedLengths = new Map<string, number>();
|
|
10
|
+
private readonly responseRecordedLengths = new Map<string, number>();
|
|
8
11
|
|
|
9
12
|
reset(): void {
|
|
10
13
|
this.segments = [];
|
|
11
14
|
this.currentSegment = [];
|
|
12
15
|
this.buffers.clear();
|
|
13
16
|
this.flushedLengths.clear();
|
|
17
|
+
this.responseRecordedLengths.clear();
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
appendDelta(itemId: string | undefined, delta: string): void {
|
|
@@ -18,8 +22,14 @@ export class CodexAgentMessageCapture {
|
|
|
18
22
|
this.buffers.set(itemId, `${this.buffers.get(itemId) ?? ""}${delta}`);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
completeItem(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage): void {
|
|
22
|
-
|
|
25
|
+
completeItem(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage, options: CompleteItemOptions = {}): void {
|
|
26
|
+
const fullText = text ?? (itemId ? this.buffers.get(itemId) : undefined);
|
|
27
|
+
this.flushText(itemId, fullText, emit);
|
|
28
|
+
if (options.includeInResponse === false || options.phase === "commentary") {
|
|
29
|
+
this.forgetResponseItem(itemId);
|
|
30
|
+
} else {
|
|
31
|
+
this.recordResponseText(itemId, fullText);
|
|
32
|
+
}
|
|
23
33
|
if (!itemId) return;
|
|
24
34
|
this.buffers.delete(itemId);
|
|
25
35
|
this.flushedLengths.delete(itemId);
|
|
@@ -36,11 +46,14 @@ export class CodexAgentMessageCapture {
|
|
|
36
46
|
this.closeSegment(emit);
|
|
37
47
|
const finalSegment = this.segments.at(-1);
|
|
38
48
|
this.segments = [];
|
|
39
|
-
return finalSegment?.length ? finalSegment.join("\n\n").trim() : undefined;
|
|
49
|
+
return finalSegment?.length ? finalSegment.map((entry) => entry.body).join("\n\n").trim() : undefined;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
private flushPending(emit: EmitAgentMessage): void {
|
|
43
|
-
for (const [itemId, text] of [...this.buffers.entries()])
|
|
53
|
+
for (const [itemId, text] of [...this.buffers.entries()]) {
|
|
54
|
+
this.flushText(itemId, text, emit);
|
|
55
|
+
this.recordResponseText(itemId, text);
|
|
56
|
+
}
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
private flushText(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage): void {
|
|
@@ -50,7 +63,30 @@ export class CodexAgentMessageCapture {
|
|
|
50
63
|
if (itemId) this.flushedLengths.set(itemId, text.length);
|
|
51
64
|
const body = chunk.trim();
|
|
52
65
|
if (!body) return;
|
|
53
|
-
this.currentSegment.push(body);
|
|
54
66
|
emit(body);
|
|
55
67
|
}
|
|
68
|
+
|
|
69
|
+
private recordResponseText(itemId: string | undefined, text: string | undefined): void {
|
|
70
|
+
if (!text) return;
|
|
71
|
+
const body = text.trim();
|
|
72
|
+
if (!body) return;
|
|
73
|
+
if (itemId) {
|
|
74
|
+
const offset = this.responseRecordedLengths.get(itemId) ?? 0;
|
|
75
|
+
const chunk = text.slice(offset).trim();
|
|
76
|
+
this.responseRecordedLengths.set(itemId, text.length);
|
|
77
|
+
if (!chunk) return;
|
|
78
|
+
this.currentSegment.push({ itemId, body: chunk });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.currentSegment.push({ body });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private forgetResponseItem(itemId: string | undefined): void {
|
|
85
|
+
if (!itemId) return;
|
|
86
|
+
this.responseRecordedLengths.delete(itemId);
|
|
87
|
+
this.currentSegment = this.currentSegment.filter((entry) => entry.itemId !== itemId);
|
|
88
|
+
this.segments = this.segments
|
|
89
|
+
.map((segment) => segment.filter((entry) => entry.itemId !== itemId))
|
|
90
|
+
.filter((segment) => segment.length > 0);
|
|
91
|
+
}
|
|
56
92
|
}
|
package/src/adapters/codex.ts
CHANGED
|
@@ -504,7 +504,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
504
504
|
// buffered reasoning first so it lands in transcript order, ahead of this tool/message.
|
|
505
505
|
if (type !== "reasoning") this.flushBufferedReasoning();
|
|
506
506
|
if (type === "agentMessage") {
|
|
507
|
-
this.agentMessageCapture.completeItem(itemId, stringValue(item.text), (body) => this.emitAgentNarration(body));
|
|
507
|
+
this.agentMessageCapture.completeItem(itemId, stringValue(item.text), (body) => this.emitAgentNarration(body), { phase: item.phase });
|
|
508
508
|
return;
|
|
509
509
|
}
|
|
510
510
|
if (type === "userMessage") {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { RelayHttpClient } from "agent-relay-sdk";
|
|
2
|
+
import { quotaStateFromProbeMetrics } from "agent-relay-sdk/context-probe";
|
|
3
|
+
import { readRunnerContextProbeState } from "./context-probe-state";
|
|
4
|
+
import { DEFAULT_PROVIDER_QUOTA_CONFIG, normalizeProviderQuotaConfig, resolveClaudeStatuslineQuotaIdentity } from "./quota";
|
|
5
|
+
|
|
6
|
+
const PASSIVE_QUOTA_CONFIG_REFRESH_MS = 60_000;
|
|
7
|
+
|
|
8
|
+
interface ClaudeQuotaHarvestOptions {
|
|
9
|
+
agentId: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
http: () => Pick<RelayHttpClient, "getProviderQuotaConfig" | "upsertProviderQuota">;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ClaudeQuotaHarvest {
|
|
15
|
+
private config = { ...DEFAULT_PROVIDER_QUOTA_CONFIG };
|
|
16
|
+
private configRefreshAt = 0;
|
|
17
|
+
private lastReportAt?: number;
|
|
18
|
+
|
|
19
|
+
constructor(private readonly options: ClaudeQuotaHarvestOptions) {}
|
|
20
|
+
|
|
21
|
+
async publish(): Promise<void> {
|
|
22
|
+
if (this.options.provider !== "claude") return;
|
|
23
|
+
const probeMetrics = readRunnerContextProbeState(this.options.agentId);
|
|
24
|
+
const quota = probeMetrics ? quotaStateFromProbeMetrics(probeMetrics) : undefined;
|
|
25
|
+
if (!quota) return;
|
|
26
|
+
const config = await this.refreshConfig();
|
|
27
|
+
if (!config.enabled) return;
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (this.lastReportAt !== undefined && now - this.lastReportAt < config.pollIntervalMs) return;
|
|
30
|
+
const identity = await resolveClaudeStatuslineQuotaIdentity({ env: process.env });
|
|
31
|
+
if (!identity) return;
|
|
32
|
+
await this.options.http().upsertProviderQuota({
|
|
33
|
+
provider: "claude",
|
|
34
|
+
accountKey: identity.accountKey,
|
|
35
|
+
quota,
|
|
36
|
+
lastAttemptAt: quota.updatedAt,
|
|
37
|
+
sourceAgentId: this.options.agentId,
|
|
38
|
+
});
|
|
39
|
+
this.lastReportAt = now;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async refreshConfig() {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (now < this.configRefreshAt) return this.config;
|
|
45
|
+
this.configRefreshAt = now + PASSIVE_QUOTA_CONFIG_REFRESH_MS;
|
|
46
|
+
try {
|
|
47
|
+
const map = await this.options.http().getProviderQuotaConfig();
|
|
48
|
+
this.config = map.claude
|
|
49
|
+
? normalizeProviderQuotaConfig(map.claude)
|
|
50
|
+
: { ...DEFAULT_PROVIDER_QUOTA_CONFIG };
|
|
51
|
+
} catch {
|
|
52
|
+
// Keep the previous config; defaults preserve collection if the relay is older
|
|
53
|
+
// or temporarily unreachable during a liveness tick.
|
|
54
|
+
}
|
|
55
|
+
return this.config;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ManagedProcess } from "./adapter";
|
|
2
|
+
|
|
3
|
+
export function providerTerminalSession(process?: ManagedProcess): string | undefined {
|
|
4
|
+
return typeof process?.meta?.tmuxSession === "string" ? process.meta.tmuxSession : undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function providerTerminalSocket(process?: ManagedProcess): string | undefined {
|
|
8
|
+
return typeof process?.meta?.tmuxSocket === "string" ? process.meta.tmuxSocket : undefined;
|
|
9
|
+
}
|
package/src/quota.ts
CHANGED
|
@@ -4,6 +4,7 @@ export {
|
|
|
4
4
|
CLAUDE_MESSAGES_URL,
|
|
5
5
|
CLAUDE_QUOTA_PROBE_MODEL,
|
|
6
6
|
CLAUDE_USAGE_URL,
|
|
7
|
+
DEFAULT_PROVIDER_QUOTA_CONFIG,
|
|
7
8
|
QUOTA_FAILURE_LOG_INTERVAL_MS,
|
|
8
9
|
QUOTA_FAST_RETRY_MS,
|
|
9
10
|
QUOTA_POLL_INTERVAL_MS,
|
|
@@ -20,11 +21,13 @@ export {
|
|
|
20
21
|
defaultClaudeConfigDir,
|
|
21
22
|
defaultClaudeCredentialsPath,
|
|
22
23
|
hostProviderAccountKey,
|
|
24
|
+
normalizeProviderQuotaConfig,
|
|
23
25
|
providerPayloadAccountKey,
|
|
24
26
|
providerQuotaErrorFromCollectorError,
|
|
25
27
|
quotaRetryAfterMs,
|
|
26
28
|
readClaudeOAuthAccessToken,
|
|
27
29
|
resolveClaudeQuotaIdentity,
|
|
30
|
+
resolveClaudeStatuslineQuotaIdentity,
|
|
28
31
|
resolveCodexQuotaIdentity,
|
|
29
32
|
resolveCodexQuotaIdentityFromHome,
|
|
30
33
|
resolveStableClaudeQuotaIdentity,
|
package/src/runner-core.ts
CHANGED
|
@@ -4,11 +4,13 @@ import { readFile } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import type { AgentLifecycle, AgentProfile, ContextState, Message, MessageSessionMeta, SendMessageInput, TaskStatusInput, WorkspaceMetadata } from "agent-relay-sdk";
|
|
6
6
|
import { errMessage, RelayBusClient, RelayHttpClient } from "agent-relay-sdk";
|
|
7
|
-
import { contextStateFromProbeMetrics, quotaStateFromProbeMetrics
|
|
7
|
+
import { contextStateFromProbeMetrics, quotaStateFromProbeMetrics } from "agent-relay-sdk/context-probe";
|
|
8
8
|
import { providerAttachmentText, providerMessageText } from "./adapter";
|
|
9
9
|
import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissionDecision, ProviderPermissionDecisionInput, ProviderSessionEvent, ProviderStatusUpdate, RunnerSpawnConfig, SemanticStatus, TerminalAttachSpec } from "./adapter";
|
|
10
10
|
import { messagesWithCachedAttachments } from "./attachment-cache";
|
|
11
11
|
import { ClaimTracker } from "./claim-tracker";
|
|
12
|
+
import { ClaudeQuotaHarvest } from "./claude-quota-harvest";
|
|
13
|
+
import { readRunnerContextProbeState } from "./context-probe-state";
|
|
12
14
|
import { startControlServer, type ControlServer } from "./control-server";
|
|
13
15
|
import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
|
|
14
16
|
import { Outbox, type OutboxRecord } from "./outbox";
|
|
@@ -24,7 +26,8 @@ import { RunnerInsights } from "./runner-insights";
|
|
|
24
26
|
import { BusyReconciler } from "./busy-reconciler";
|
|
25
27
|
import { publishCapturedResponse } from "./response-capture-report";
|
|
26
28
|
import { runnerLaunchInjectionSessionEvents, type RunnerRelayInjectionEvent } from "./relay-injection-events";
|
|
27
|
-
import {
|
|
29
|
+
import { providerTerminalSession, providerTerminalSocket } from "./process-meta";
|
|
30
|
+
import { capsFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
|
|
28
31
|
import {
|
|
29
32
|
appliedAgentProfileMetadata,
|
|
30
33
|
commandTimeoutMs,
|
|
@@ -204,6 +207,7 @@ export class AgentRunner {
|
|
|
204
207
|
private readonly sessionOutbox: Outbox;
|
|
205
208
|
private readonly insights: RunnerInsights;
|
|
206
209
|
private readonly busyReconciler: BusyReconciler;
|
|
210
|
+
private readonly claudeQuotaHarvest: ClaudeQuotaHarvest;
|
|
207
211
|
private currentToken?: string;
|
|
208
212
|
private currentTokenJti?: string;
|
|
209
213
|
private currentTokenProfileId?: string;
|
|
@@ -314,6 +318,7 @@ export class AgentRunner {
|
|
|
314
318
|
this.mcpProxySecret = crypto.randomUUID();
|
|
315
319
|
const runtime = runtimeMetadata(options.provider);
|
|
316
320
|
this.http = new RelayHttpClient({ baseUrl: options.relayUrl, token: this.currentToken });
|
|
321
|
+
this.claudeQuotaHarvest = new ClaudeQuotaHarvest({ agentId: this.agentId, provider: options.provider, http: () => this.http });
|
|
317
322
|
this.obligationCache = new ReplyObligationCache({ fetch: () => this.http.listReplyObligations(this.agentId) });
|
|
318
323
|
// Co-locate the durable outbox with the runner's runtime state (survives reboot) when the
|
|
319
324
|
// orchestrator told us where that is; otherwise the Outbox falls back to a temp dir.
|
|
@@ -386,8 +391,8 @@ export class AgentRunner {
|
|
|
386
391
|
agentProfile: options.agentProfile ? appliedAgentProfileMetadata(options.provider, options.agentProfile, { ...options, agentId: this.agentId, env: {}, controlPort: 0 } as RunnerSpawnConfig, options.providerConfig) : null,
|
|
387
392
|
runnerId: options.runnerId,
|
|
388
393
|
startedAt: options.startedAt,
|
|
389
|
-
tmuxSession: this.
|
|
390
|
-
tmuxSocket: this.
|
|
394
|
+
tmuxSession: providerTerminalSession(this.process) ?? options.tmuxSession ?? null,
|
|
395
|
+
tmuxSocket: providerTerminalSocket(this.process) ?? null,
|
|
391
396
|
policyName: options.policyName ?? null,
|
|
392
397
|
spawnRequestId: options.spawnRequestId ?? null,
|
|
393
398
|
automationId: options.automationId ?? null,
|
|
@@ -628,8 +633,8 @@ export class AgentRunner {
|
|
|
628
633
|
provider: this.options.provider,
|
|
629
634
|
controlUrl: this.control.url,
|
|
630
635
|
pid: process.pid,
|
|
631
|
-
...(this.
|
|
632
|
-
...(this.
|
|
636
|
+
...(providerTerminalSession(this.process) ? { tmuxSession: providerTerminalSession(this.process) } : {}),
|
|
637
|
+
...(providerTerminalSocket(this.process) ? { tmuxSocket: providerTerminalSocket(this.process) } : {}),
|
|
633
638
|
startedAt: this.options.startedAt,
|
|
634
639
|
}, null, 2) + "\n", { mode: 0o600 });
|
|
635
640
|
} catch (error) {
|
|
@@ -1807,8 +1812,8 @@ export class AgentRunner {
|
|
|
1807
1812
|
meta: {
|
|
1808
1813
|
runnerId: this.options.runnerId,
|
|
1809
1814
|
startedAt: this.options.startedAt,
|
|
1810
|
-
tmuxSession: this.
|
|
1811
|
-
tmuxSocket: this.
|
|
1815
|
+
tmuxSession: providerTerminalSession(this.process) ?? this.options.tmuxSession ?? null,
|
|
1816
|
+
tmuxSocket: providerTerminalSocket(this.process) ?? null,
|
|
1812
1817
|
policyName: this.options.policyName ?? null,
|
|
1813
1818
|
spawnRequestId: this.options.spawnRequestId ?? null,
|
|
1814
1819
|
automationId: this.options.automationId ?? null,
|
|
@@ -1857,6 +1862,7 @@ export class AgentRunner {
|
|
|
1857
1862
|
await this.http.setStatus(this.agentId, agentStatus, this.options.instanceId);
|
|
1858
1863
|
await this.http.setReady(this.agentId, agentStatus !== "offline", this.options.instanceId);
|
|
1859
1864
|
await this.http.heartbeat(this.agentId, this.options.instanceId);
|
|
1865
|
+
await this.claudeQuotaHarvest.publish().catch((error) => this.logRunnerDiagnostic(`passive Claude quota publish failed: ${errMessage(error)}`));
|
|
1860
1866
|
await this.bootstrapUnreadMessages();
|
|
1861
1867
|
} catch (error) {
|
|
1862
1868
|
if (!this.stopped) this.handleHttpLivenessFailure(error);
|
|
@@ -2111,8 +2117,8 @@ export class AgentRunner {
|
|
|
2111
2117
|
const probeContext = probeMetrics ? contextStateFromProbeMetrics(probeMetrics) : undefined;
|
|
2112
2118
|
const context = processContext ?? probeContext;
|
|
2113
2119
|
const quota = probeMetrics ? quotaStateFromProbeMetrics(probeMetrics) : undefined;
|
|
2114
|
-
const terminalSession = this.
|
|
2115
|
-
const terminalSocket = this.
|
|
2120
|
+
const terminalSession = providerTerminalSession(this.process);
|
|
2121
|
+
const terminalSocket = providerTerminalSocket(this.process);
|
|
2116
2122
|
const probeModel: ProbeModelInfo | undefined = probeMetrics?.model || probeMetrics?.effort
|
|
2117
2123
|
? { model: probeMetrics.model, effort: probeMetrics.effort }
|
|
2118
2124
|
: undefined;
|
|
@@ -2125,14 +2131,6 @@ export class AgentRunner {
|
|
|
2125
2131
|
return meta;
|
|
2126
2132
|
}
|
|
2127
2133
|
|
|
2128
|
-
private providerTerminalSession(): string | undefined {
|
|
2129
|
-
return typeof this.process?.meta?.tmuxSession === "string" ? this.process.meta.tmuxSession : undefined;
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
private providerTerminalSocket(): string | undefined {
|
|
2133
|
-
return typeof this.process?.meta?.tmuxSocket === "string" ? this.process.meta.tmuxSocket : undefined;
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
2134
|
private latestProcessContext(): ContextState | undefined {
|
|
2137
2135
|
const getContext = this.process?.meta?.getContext;
|
|
2138
2136
|
if (typeof getContext === "function") {
|
|
@@ -2144,8 +2142,7 @@ export class AgentRunner {
|
|
|
2144
2142
|
}
|
|
2145
2143
|
|
|
2146
2144
|
private latestProbeMetrics() {
|
|
2147
|
-
|
|
2148
|
-
return readContextProbeState(this.agentId, stateDir);
|
|
2145
|
+
return readRunnerContextProbeState(this.agentId);
|
|
2149
2146
|
}
|
|
2150
2147
|
|
|
2151
2148
|
private matchesMessage(message: Message): boolean {
|