agent-relay-runner 0.74.0 → 0.76.0
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-client.ts +9 -0
- package/src/quota.ts +283 -0
- package/src/runner-core.ts +10 -8
- package/src/runner-helpers.ts +22 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
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.51"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
|
@@ -74,6 +74,11 @@ interface ThreadLoadedListResponse {
|
|
|
74
74
|
nextCursor: string | null;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
interface AccountRateLimitsResponse {
|
|
78
|
+
rateLimits?: unknown;
|
|
79
|
+
rate_limits?: unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
77
82
|
export const CODEX_APP_CLIENT_EVENT_CAP = 5_000;
|
|
78
83
|
|
|
79
84
|
export class CodexAppClient {
|
|
@@ -193,6 +198,10 @@ export class CodexAppClient {
|
|
|
193
198
|
return this.request<Record<string, never>>("turn/interrupt", { threadId, turnId });
|
|
194
199
|
}
|
|
195
200
|
|
|
201
|
+
async accountRateLimitsRead(): Promise<AccountRateLimitsResponse> {
|
|
202
|
+
return this.request<AccountRateLimitsResponse>("account/rateLimits/read");
|
|
203
|
+
}
|
|
204
|
+
|
|
196
205
|
respondToServerRequest(id: JsonRpcId, result: unknown): void {
|
|
197
206
|
if (!this.connected) {
|
|
198
207
|
throw new Error("websocket not connected");
|
package/src/quota.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ContextProbeMetrics, QuotaState, QuotaWindowName } from "agent-relay-sdk";
|
|
5
|
+
import { errMessage, isRecord, quotaStateFromProbeMetrics, stringValue } from "agent-relay-sdk";
|
|
6
|
+
import type { ManagedProcess } from "./adapter";
|
|
7
|
+
|
|
8
|
+
const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
9
|
+
const CLAUDE_OAUTH_BETA = "oauth-2025-04-20";
|
|
10
|
+
const QUOTA_POLL_INTERVAL_MS = 15 * 60 * 1000;
|
|
11
|
+
const QUOTA_FAILURE_LOG_INTERVAL_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
type CodexQuotaClient = {
|
|
14
|
+
isConnected?: () => boolean;
|
|
15
|
+
accountRateLimitsRead?: () => Promise<unknown>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class RunnerQuotaPoller {
|
|
19
|
+
private quotaState?: QuotaState;
|
|
20
|
+
private timer?: Timer;
|
|
21
|
+
private inFlight = false;
|
|
22
|
+
private active = false;
|
|
23
|
+
private lastLog?: { key: string; at: number };
|
|
24
|
+
|
|
25
|
+
constructor(private readonly options: {
|
|
26
|
+
provider: string;
|
|
27
|
+
agentId: string;
|
|
28
|
+
getProcess: () => ManagedProcess | undefined;
|
|
29
|
+
onUpdate: () => void;
|
|
30
|
+
log: (message: string) => void;
|
|
31
|
+
}) {}
|
|
32
|
+
|
|
33
|
+
get current(): QuotaState | undefined {
|
|
34
|
+
return this.quotaState;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
start(): void {
|
|
38
|
+
this.stop();
|
|
39
|
+
if (!quotaProviderSupported(this.options.provider)) return;
|
|
40
|
+
this.active = true;
|
|
41
|
+
void this.refresh();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
stop(): void {
|
|
45
|
+
this.active = false;
|
|
46
|
+
if (this.timer) clearTimeout(this.timer);
|
|
47
|
+
this.timer = undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
clear(): void {
|
|
51
|
+
this.quotaState = undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private schedule(delayMs = QUOTA_POLL_INTERVAL_MS): void {
|
|
55
|
+
if (this.timer) clearTimeout(this.timer);
|
|
56
|
+
this.timer = undefined;
|
|
57
|
+
if (!this.active || !quotaProviderSupported(this.options.provider)) return;
|
|
58
|
+
this.timer = setTimeout(() => {
|
|
59
|
+
this.timer = undefined;
|
|
60
|
+
void this.refresh();
|
|
61
|
+
}, Math.max(1_000, delayMs));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async refresh(): Promise<void> {
|
|
65
|
+
if (this.inFlight) return;
|
|
66
|
+
this.inFlight = true;
|
|
67
|
+
try {
|
|
68
|
+
const quota = await collectRunnerQuotaState({
|
|
69
|
+
provider: this.options.provider,
|
|
70
|
+
agentId: this.options.agentId,
|
|
71
|
+
process: this.options.getProcess(),
|
|
72
|
+
});
|
|
73
|
+
if (quota && this.active) {
|
|
74
|
+
this.quotaState = quota;
|
|
75
|
+
this.options.onUpdate();
|
|
76
|
+
}
|
|
77
|
+
this.schedule();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const retryAfterMs = quotaRetryAfterMs(error);
|
|
80
|
+
if (this.active) this.logFailure(error, retryAfterMs);
|
|
81
|
+
this.schedule(retryAfterMs ?? QUOTA_POLL_INTERVAL_MS);
|
|
82
|
+
} finally {
|
|
83
|
+
this.inFlight = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private logFailure(error: unknown, retryAfterMs: number | undefined): void {
|
|
88
|
+
const key = retryAfterMs !== undefined ? `retry-after:${retryAfterMs}` : errMessage(error);
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
if (this.lastLog?.key === key && now - this.lastLog.at < QUOTA_FAILURE_LOG_INTERVAL_MS) return;
|
|
91
|
+
this.lastLog = { key, at: now };
|
|
92
|
+
const suffix = retryAfterMs !== undefined ? `; retrying in ${Math.round(retryAfterMs / 1000)}s` : "";
|
|
93
|
+
this.options.log(`quota refresh failed${suffix}: ${errMessage(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class QuotaRetryAfterError extends Error {
|
|
98
|
+
constructor(readonly retryAfterMs: number, message = "quota source requested retry backoff") {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = "QuotaRetryAfterError";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function quotaRetryAfterMs(error: unknown): number | undefined {
|
|
105
|
+
return error instanceof QuotaRetryAfterError ? error.retryAfterMs : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function collectRunnerQuotaState(input: {
|
|
109
|
+
provider: string;
|
|
110
|
+
agentId: string;
|
|
111
|
+
process?: ManagedProcess;
|
|
112
|
+
}): Promise<QuotaState | undefined> {
|
|
113
|
+
if (input.provider === "claude") return collectClaudeQuotaState({ agentId: input.agentId });
|
|
114
|
+
if (input.provider !== "codex") return undefined;
|
|
115
|
+
const client = input.process?.meta?.client as CodexQuotaClient | undefined;
|
|
116
|
+
if (!client?.isConnected?.() || typeof client.accountRateLimitsRead !== "function") return undefined;
|
|
117
|
+
return collectCodexQuotaState({
|
|
118
|
+
agentId: codexProcessAgentId(input.process, input.agentId),
|
|
119
|
+
rateLimitsRead: () => client.accountRateLimitsRead!(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function collectClaudeQuotaState(options: {
|
|
124
|
+
agentId: string;
|
|
125
|
+
credentialsPath?: string;
|
|
126
|
+
fetchImpl?: typeof fetch;
|
|
127
|
+
now?: number;
|
|
128
|
+
}): Promise<QuotaState | undefined> {
|
|
129
|
+
const accessToken = await readClaudeOAuthAccessToken(options.credentialsPath ?? defaultClaudeCredentialsPath());
|
|
130
|
+
if (!accessToken) return undefined;
|
|
131
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
132
|
+
const response = await fetchImpl(CLAUDE_USAGE_URL, {
|
|
133
|
+
method: "GET",
|
|
134
|
+
headers: {
|
|
135
|
+
authorization: `Bearer ${accessToken}`,
|
|
136
|
+
"anthropic-beta": CLAUDE_OAUTH_BETA,
|
|
137
|
+
},
|
|
138
|
+
signal: AbortSignal.timeout(10_000),
|
|
139
|
+
});
|
|
140
|
+
if (response.status === 429) {
|
|
141
|
+
throw new QuotaRetryAfterError(retryAfterHeaderMs(response.headers.get("retry-after")) ?? 60_000, "Claude quota source returned 429");
|
|
142
|
+
}
|
|
143
|
+
if (!response.ok) return undefined;
|
|
144
|
+
return claudeUsageQuotaState(await response.json(), options.agentId, options.now ?? Date.now());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function claudeUsageQuotaState(payload: unknown, agentId: string, now = Date.now()): QuotaState | undefined {
|
|
148
|
+
return quotaStateFromProbeMetrics({
|
|
149
|
+
agentId,
|
|
150
|
+
contextPercent: 0,
|
|
151
|
+
quotaWindows: [
|
|
152
|
+
quotaWindowFromAnthropicUsage("five_hour", payload),
|
|
153
|
+
quotaWindowFromAnthropicUsage("seven_day", payload),
|
|
154
|
+
].filter((window): window is NonNullable<ContextProbeMetrics["quotaWindows"]>[number] => Boolean(window)),
|
|
155
|
+
source: "api",
|
|
156
|
+
confidence: "reported",
|
|
157
|
+
timestamp: now,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function collectCodexQuotaState(input: {
|
|
162
|
+
agentId: string;
|
|
163
|
+
rateLimitsRead: () => Promise<unknown>;
|
|
164
|
+
now?: number;
|
|
165
|
+
}): Promise<QuotaState | undefined> {
|
|
166
|
+
try {
|
|
167
|
+
return codexRateLimitsQuotaState(await input.rateLimitsRead(), input.agentId, input.now ?? Date.now());
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const retryAfter = codexRetryAfterMs(error);
|
|
170
|
+
if (retryAfter !== undefined) throw new QuotaRetryAfterError(retryAfter, "Codex quota source returned 429");
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function codexRateLimitsQuotaState(payload: unknown, agentId: string, now = Date.now()): QuotaState | undefined {
|
|
176
|
+
const rateLimits = isRecord(payload) && isRecord(payload.rateLimits) ? payload.rateLimits
|
|
177
|
+
: isRecord(payload) && isRecord(payload.rate_limits) ? payload.rate_limits
|
|
178
|
+
: payload;
|
|
179
|
+
return quotaStateFromProbeMetrics({
|
|
180
|
+
agentId,
|
|
181
|
+
contextPercent: 0,
|
|
182
|
+
quotaWindows: [
|
|
183
|
+
quotaWindowFromCodexRateLimit("primary", rateLimits),
|
|
184
|
+
quotaWindowFromCodexRateLimit("secondary", rateLimits),
|
|
185
|
+
].filter((window): window is NonNullable<ContextProbeMetrics["quotaWindows"]>[number] => Boolean(window)),
|
|
186
|
+
source: "api",
|
|
187
|
+
confidence: "reported",
|
|
188
|
+
timestamp: now,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function defaultClaudeCredentialsPath(): string {
|
|
193
|
+
return join(homedir(), ".claude", ".credentials.json");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function quotaProviderSupported(provider: string): boolean {
|
|
197
|
+
return provider === "claude" || provider === "codex";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function codexProcessAgentId(process: ManagedProcess | undefined, fallback: string): string {
|
|
201
|
+
const config = isRecord(process?.meta?.config) ? process.meta.config : undefined;
|
|
202
|
+
return stringValue(config?.agentId) ?? fallback;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function readClaudeOAuthAccessToken(path: string): Promise<string | undefined> {
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
208
|
+
const oauth = isRecord(parsed) && isRecord(parsed.claudeAiOauth) ? parsed.claudeAiOauth : undefined;
|
|
209
|
+
const token = oauth?.accessToken;
|
|
210
|
+
return typeof token === "string" && token.trim() ? token : undefined;
|
|
211
|
+
} catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function quotaWindowFromAnthropicUsage(name: QuotaWindowName, payload: unknown): NonNullable<ContextProbeMetrics["quotaWindows"]>[number] | undefined {
|
|
217
|
+
if (!isRecord(payload)) return undefined;
|
|
218
|
+
const raw = isRecord(payload[name]) ? payload[name] : undefined;
|
|
219
|
+
const utilization = normalizePercentLike(raw?.utilization);
|
|
220
|
+
if (utilization === undefined) return undefined;
|
|
221
|
+
return {
|
|
222
|
+
name,
|
|
223
|
+
utilization,
|
|
224
|
+
...(resetValueMs(raw?.resets_at) !== undefined ? { resetsAt: resetValueMs(raw?.resets_at)! } : {}),
|
|
225
|
+
unit: "%",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function quotaWindowFromCodexRateLimit(name: QuotaWindowName, payload: unknown): NonNullable<ContextProbeMetrics["quotaWindows"]>[number] | undefined {
|
|
230
|
+
if (!isRecord(payload)) return undefined;
|
|
231
|
+
const raw = isRecord(payload[name]) ? payload[name] : undefined;
|
|
232
|
+
const utilization = normalizePercentLike(raw?.usedPercent ?? raw?.used_percent ?? raw?.utilization);
|
|
233
|
+
if (utilization === undefined) return undefined;
|
|
234
|
+
return {
|
|
235
|
+
name,
|
|
236
|
+
utilization,
|
|
237
|
+
...(resetValueMs(raw?.resetsAt ?? raw?.resets_at) !== undefined ? { resetsAt: resetValueMs(raw?.resetsAt ?? raw?.resets_at)! } : {}),
|
|
238
|
+
unit: "%",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function normalizePercentLike(value: unknown): number | undefined {
|
|
243
|
+
const number = numberValue(value);
|
|
244
|
+
if (number === undefined) return undefined;
|
|
245
|
+
const fraction = number > 1 ? number / 100 : number;
|
|
246
|
+
return Math.max(0, Math.min(1, fraction));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function resetValueMs(value: unknown): number | undefined {
|
|
250
|
+
const number = numberValue(value);
|
|
251
|
+
if (number !== undefined) return number < 10_000_000_000 ? number * 1000 : number;
|
|
252
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
253
|
+
const parsed = Date.parse(value);
|
|
254
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function retryAfterHeaderMs(value: string | null): number | undefined {
|
|
258
|
+
if (!value) return undefined;
|
|
259
|
+
const seconds = Number(value);
|
|
260
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
|
|
261
|
+
const date = Date.parse(value);
|
|
262
|
+
if (!Number.isFinite(date)) return undefined;
|
|
263
|
+
return Math.max(0, date - Date.now());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function codexRetryAfterMs(error: unknown): number | undefined {
|
|
267
|
+
const message = errMessage(error);
|
|
268
|
+
if (!/\b429\b/.test(message)) return undefined;
|
|
269
|
+
return retryAfterHeaderMs(retryAfterFromText(message)) ?? 60_000;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function retryAfterFromText(message: string): string | null {
|
|
273
|
+
return /retry-after[:=]\s*([^,;\s]+)/i.exec(message)?.[1] ?? null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function numberValue(value: unknown): number | undefined {
|
|
277
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
278
|
+
if (typeof value === "string" && value.trim()) {
|
|
279
|
+
const parsed = Number(value);
|
|
280
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
package/src/runner-core.ts
CHANGED
|
@@ -4,7 +4,7 @@ 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, readContextProbeState } from "agent-relay-sdk/context-probe";
|
|
7
|
+
import { contextStateFromProbeMetrics, quotaStateFromProbeMetrics, readContextProbeState } 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";
|
|
@@ -24,6 +24,7 @@ import { RunnerInsights } from "./runner-insights";
|
|
|
24
24
|
import { BusyReconciler } from "./busy-reconciler";
|
|
25
25
|
import { publishCapturedResponse } from "./response-capture-report";
|
|
26
26
|
import { runnerLaunchInjectionSessionEvents, type RunnerRelayInjectionEvent } from "./relay-injection-events";
|
|
27
|
+
import { RunnerQuotaPoller } from "./quota";
|
|
27
28
|
import { capsFromEnv, contextStateDirFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
|
|
28
29
|
import {
|
|
29
30
|
appliedAgentProfileMetadata,
|
|
@@ -204,6 +205,7 @@ export class AgentRunner {
|
|
|
204
205
|
private readonly sessionOutbox: Outbox;
|
|
205
206
|
private readonly insights: RunnerInsights;
|
|
206
207
|
private readonly busyReconciler: BusyReconciler;
|
|
208
|
+
private readonly quotaPoller: RunnerQuotaPoller;
|
|
207
209
|
private currentToken?: string;
|
|
208
210
|
private currentTokenJti?: string;
|
|
209
211
|
private currentTokenProfileId?: string;
|
|
@@ -351,6 +353,7 @@ export class AgentRunner {
|
|
|
351
353
|
clearProviderTurn: (reason) => this.forceClearProviderTurn(reason),
|
|
352
354
|
sessionDebug: (message) => this.sessionDebug(message),
|
|
353
355
|
});
|
|
356
|
+
this.quotaPoller = new RunnerQuotaPoller({ provider: options.provider, agentId: this.agentId, getProcess: () => this.process, onUpdate: () => this.publishStatus(), log: (message) => this.logRunnerDiagnostic(message) });
|
|
354
357
|
this.bus = new RelayBusClient({
|
|
355
358
|
url: relayBusUrl(options.relayUrl),
|
|
356
359
|
role: "provider",
|
|
@@ -461,7 +464,7 @@ export class AgentRunner {
|
|
|
461
464
|
void this.sweepStaleScratch();
|
|
462
465
|
this.process = await this.spawnProvider();
|
|
463
466
|
this.writeRunnerInfoFile();
|
|
464
|
-
this.processStartedAt = Date.now();
|
|
467
|
+
this.processStartedAt = Date.now(); this.quotaPoller.start();
|
|
465
468
|
this.publishStatus();
|
|
466
469
|
await this.deliverInitialPrompt();
|
|
467
470
|
await this.bootstrapUnreadMessages();
|
|
@@ -493,7 +496,7 @@ export class AgentRunner {
|
|
|
493
496
|
if (this.httpLivenessTimer) clearInterval(this.httpLivenessTimer);
|
|
494
497
|
this.httpLivenessTimer = undefined;
|
|
495
498
|
if (this.tokenRenewTimer) clearTimeout(this.tokenRenewTimer);
|
|
496
|
-
this.tokenRenewTimer = undefined;
|
|
499
|
+
this.tokenRenewTimer = undefined; this.quotaPoller.stop();
|
|
497
500
|
this.busyReconciler.disarm();
|
|
498
501
|
this.stopReasoningTail();
|
|
499
502
|
this.obligationCache.stop();
|
|
@@ -934,6 +937,7 @@ export class AgentRunner {
|
|
|
934
937
|
if (this.stopped) return;
|
|
935
938
|
this.process = await this.spawnProvider();
|
|
936
939
|
this.processStartedAt = Date.now();
|
|
940
|
+
this.quotaPoller.clear(); this.quotaPoller.start();
|
|
937
941
|
} finally {
|
|
938
942
|
this.restartInProgress = false;
|
|
939
943
|
}
|
|
@@ -2110,21 +2114,19 @@ export class AgentRunner {
|
|
|
2110
2114
|
const probeMetrics = this.latestProbeMetrics();
|
|
2111
2115
|
const probeContext = probeMetrics ? contextStateFromProbeMetrics(probeMetrics) : undefined;
|
|
2112
2116
|
const context = processContext ?? probeContext;
|
|
2117
|
+
const quota = this.quotaPoller.current ?? (probeMetrics ? quotaStateFromProbeMetrics(probeMetrics) : undefined);
|
|
2113
2118
|
const terminalSession = this.providerTerminalSession();
|
|
2114
2119
|
const terminalSocket = this.providerTerminalSocket();
|
|
2115
2120
|
const probeModel: ProbeModelInfo | undefined = probeMetrics?.model || probeMetrics?.effort
|
|
2116
2121
|
? { model: probeMetrics.model, effort: probeMetrics.effort }
|
|
2117
2122
|
: undefined;
|
|
2118
2123
|
const meta: Record<string, unknown> = {
|
|
2119
|
-
providerCapabilities: runtimeProviderCapabilities(
|
|
2120
|
-
this.options,
|
|
2121
|
-
context,
|
|
2122
|
-
probeModel,
|
|
2123
|
-
),
|
|
2124
|
+
providerCapabilities: runtimeProviderCapabilities(this.options, context, probeModel, quota),
|
|
2124
2125
|
...(terminalSession ? { tmuxSession: terminalSession } : {}),
|
|
2125
2126
|
...(terminalSocket ? { tmuxSocket: terminalSocket } : {}),
|
|
2126
2127
|
};
|
|
2127
2128
|
if (context) meta.context = context;
|
|
2129
|
+
if (quota) meta.quota = quota;
|
|
2128
2130
|
return meta;
|
|
2129
2131
|
}
|
|
2130
2132
|
|
package/src/runner-helpers.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
2
|
-
import type { AgentProfile, ContextState, Message, ProviderCapabilities } from "agent-relay-sdk";
|
|
2
|
+
import type { AgentProfile, ContextState, Message, ProviderCapabilities, QuotaState } from "agent-relay-sdk";
|
|
3
3
|
import { errMessage } from "agent-relay-sdk";
|
|
4
4
|
import { targetMatchesAgentIdentity } from "agent-relay-sdk/agent-target";
|
|
5
5
|
import type { ProviderAdapter, ProviderConfig, RunnerSpawnConfig, SemanticStatus } from "./adapter";
|
|
@@ -192,7 +192,7 @@ export function lifecycleCapabilities(): Record<string, true> {
|
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
export function runtimeProviderCapabilities(options: RuntimeProviderOptions, contextState?: ContextState, probeModel?: ProbeModelInfo): ProviderCapabilities {
|
|
195
|
+
export function runtimeProviderCapabilities(options: RuntimeProviderOptions, contextState?: ContextState, probeModel?: ProbeModelInfo, quotaState?: QuotaState): ProviderCapabilities {
|
|
196
196
|
const model = options.model ?? probeModel?.model;
|
|
197
197
|
const effort = options.effort ?? probeModel?.effort;
|
|
198
198
|
const modelSource = options.model ? "runtime" as const : probeModel?.model ? "provider" as const : "runtime" as const;
|
|
@@ -225,6 +225,7 @@ export function runtimeProviderCapabilities(options: RuntimeProviderOptions, con
|
|
|
225
225
|
lastUpdatedAt: options.startedAt,
|
|
226
226
|
},
|
|
227
227
|
...runtimeProviderContextCapabilities(options, contextState),
|
|
228
|
+
...runtimeProviderQuotaCapabilities(options, quotaState),
|
|
228
229
|
...runtimeProviderTerminalCapabilities(options),
|
|
229
230
|
liveSession: {
|
|
230
231
|
capture: true,
|
|
@@ -324,6 +325,25 @@ function runtimeProviderContextCapabilities(options: RuntimeProviderOptions, con
|
|
|
324
325
|
return Object.keys(context).length ? { context } : {};
|
|
325
326
|
}
|
|
326
327
|
|
|
328
|
+
function runtimeProviderQuotaCapabilities(options: RuntimeProviderOptions, quotaState?: QuotaState): Pick<ProviderCapabilities, "quota"> {
|
|
329
|
+
if (options.provider !== "claude" && options.provider !== "codex") return {};
|
|
330
|
+
const fallbackWindows = options.provider === "claude"
|
|
331
|
+
? [{ name: "five_hour", unit: "%" as const, reset: true }, { name: "seven_day", unit: "%" as const, reset: true }]
|
|
332
|
+
: [{ name: "primary", unit: "%" as const, reset: true }, { name: "secondary", unit: "%" as const, reset: true }];
|
|
333
|
+
return {
|
|
334
|
+
quota: {
|
|
335
|
+
supported: true,
|
|
336
|
+
source: quotaState?.source ?? "probe",
|
|
337
|
+
unit: quotaState?.windows[0]?.unit ?? "%",
|
|
338
|
+
windows: quotaState?.windows.map((window) => ({
|
|
339
|
+
name: window.name,
|
|
340
|
+
unit: window.unit,
|
|
341
|
+
reset: window.resetsAt !== undefined,
|
|
342
|
+
})) ?? fallbackWindows,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
327
347
|
export function providerStateFromActiveWork(activeWork: Array<{ kind: string; metadata?: Record<string, unknown> }>): Record<string, unknown> | null {
|
|
328
348
|
const providerTurn = activeWork.find((item) => item.kind === "provider-turn");
|
|
329
349
|
const state = providerTurn?.metadata?.providerState;
|