feat(agentrun): add events long polling

This commit is contained in:
Codex
2026-07-15 05:47:52 +02:00
parent 7df9187a12
commit 968ffa3ecf
9 changed files with 307 additions and 17 deletions
@@ -0,0 +1,185 @@
import { rmSync } from "node:fs";
import { describe, expect, test } from "bun:test";
import { runAgentRunCommand } from "./agentrun";
const RUN_ID = "run_long_poll_fixture";
const agentRunClientYaml = [
"version: 1",
"kind: AgentRunConfig",
"metadata:",
" name: agentrun",
"manager:",
" baseUrl: __BASE_URL__",
" timeoutMs: 15000",
"auth:",
" env: HWLAB_API_KEY",
" file: /tmp/hwlab-api-key.env",
" header: Authorization",
" scheme: Bearer",
"client:",
" role: render-only",
" transport: direct-http",
" sessionPolicy:",
" tenantId: unidesk",
" projectId: test",
" providerId: NC01",
" backendProfile: codex",
" workspaceRef:",
" kind: opaque",
" executionPolicy:",
" sandbox: workspace-write",
" approval: never",
" timeoutMs: 7200000",
" network: enabled",
" secretScope:",
" allowCredentialEcho: false",
].join("\n");
function renderedText(value: Awaited<ReturnType<typeof runAgentRunCommand>>): string {
if (!("renderedText" in value) || typeof value.renderedText !== "string") throw new Error("expected rendered AgentRun CLI output");
return value.renderedText;
}
function event(seq: number): Record<string, unknown> {
return {
id: `evt_${seq}`,
seq,
type: "backend_status",
payload: { commandId: "cmd_fixture", phase: `phase-${seq}`, message: `event-${seq}` },
};
}
async function withManager(
handler: (request: Request) => Response | Promise<Response>,
action: () => Promise<void>,
): Promise<void> {
const server = Bun.serve({ port: 0, fetch: handler });
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
const previousKey = process.env.HWLAB_API_KEY;
const configPath = `/tmp/unidesk-agentrun-events-long-poll-${process.pid}-${Date.now()}.yaml`;
process.env.AGENTRUN_CLIENT_CONFIG = configPath;
process.env.HWLAB_API_KEY = "fixture-secret";
await Bun.write(configPath, agentRunClientYaml.replace("__BASE_URL__", server.url.href.replace(/\/$/u, "")));
try {
await action();
} finally {
server.stop(true);
rmSync(configPath, { force: true });
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
else process.env.HWLAB_API_KEY = previousKey;
}
}
describe.serial("AgentRun events manager-held long polling", () => {
test("forwards default 120s timeout and returns as soon as expect is reached", async () => {
let requests = 0;
await withManager(async (request) => {
requests += 1;
const url = new URL(request.url);
expect(url.pathname).toBe(`/api/v1/runs/${RUN_ID}/events`);
expect(url.searchParams.get("afterSeq")).toBe("100");
expect(url.searchParams.get("expect")).toBe("2");
expect(url.searchParams.get("timeoutMs")).toBe("120000");
expect(url.searchParams.get("limit")).toBe("21");
await Promise.all([Bun.sleep(10), Bun.sleep(20)]);
return Response.json({
ok: true,
data: {
items: [event(101), event(102)],
timedOut: false,
observed: 2,
nextAfterSeq: 102,
completionReason: "expect-reached",
},
});
}, async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "100", "--expect", "2", "-o", "json"]);
expect(result.ok).toBe(true);
const payload = JSON.parse(renderedText(result)) as Record<string, unknown>;
expect(payload).toMatchObject({ count: 2, expect: 2, timeoutMs: 120000, timedOut: false, observed: 2, nextAfterSeq: 102, completionReason: "expect-reached" });
expect(requests).toBe(1);
});
});
test("renders partial and empty timeout responses with an explicit cursor", async () => {
for (const fixture of [
{ items: [event(201)], observed: 1, nextAfterSeq: 201 },
{ items: [], observed: 0, nextAfterSeq: 200 },
]) {
await withManager((request) => {
const url = new URL(request.url);
expect(url.searchParams.get("timeoutMs")).toBe("3000");
return Response.json({ ok: true, data: { ...fixture, timedOut: true, completionReason: "timeout" } });
}, async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "200", "--expect", "10", "--timeout", "3s", "-o", "json"]);
const payload = JSON.parse(renderedText(result)) as Record<string, unknown>;
expect(payload).toMatchObject({ timedOut: true, observed: fixture.observed, nextAfterSeq: fixture.nextAfterSeq, completionReason: "timeout" });
});
}
});
test("returns concurrent event arrivals in one manager request", async () => {
let requests = 0;
await withManager(async () => {
requests += 1;
const arrivals: Record<string, unknown>[] = [];
await Promise.all([
Bun.sleep(10).then(() => arrivals.push(event(301))),
Bun.sleep(20).then(() => arrivals.push(event(302))),
]);
return Response.json({ ok: true, data: { items: arrivals, timedOut: false, observed: 2, nextAfterSeq: 302, completionReason: "expect-reached" } });
}, async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "300", "--expect", "2", "--timeout", "1m", "-o", "json"]);
const payload = JSON.parse(renderedText(result)) as { items?: Array<{ seq?: number }> };
expect(payload.items?.map((item) => item.seq)).toEqual([301, 302]);
expect(requests).toBe(1);
});
});
test("returns immediately when the target is terminal before expect is reached", async () => {
await withManager(() => Response.json({
ok: true,
data: {
items: [],
timedOut: false,
observed: 0,
nextAfterSeq: 400,
completionReason: "target-completed",
},
}), async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "400", "--expect", "10", "--timeout", "120s", "-o", "json"]);
const payload = JSON.parse(renderedText(result)) as Record<string, unknown>;
expect(payload).toMatchObject({ timedOut: false, observed: 0, nextAfterSeq: 400, completionReason: "target-completed" });
});
});
test("rejects invalid duration before request and preserves manager errors", async () => {
let requests = 0;
await withManager(() => {
requests += 1;
return Response.json({ ok: false, failureKind: "manager-unavailable", message: "event notifier unavailable" }, { status: 503 });
}, async () => {
const invalid = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "2", "--timeout", "3minutes", "-o", "json"]);
expect(invalid.ok).toBe(false);
expect(renderedText(invalid)).toContain("--timeout must use an integer duration");
expect(requests).toBe(0);
const failed = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "2", "--timeout", "1s", "-o", "json"]);
expect(failed.ok).toBe(false);
expect(renderedText(failed)).toContain("event notifier unavailable");
expect(requests).toBe(1);
});
});
test("fails visibly when a manager ignores the long-poll response contract", async () => {
await withManager(() => Response.json({ ok: true, data: { items: [], nextAfterSeq: 500 } }), async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "500", "--expect", "2", "--timeout", "1s", "-o", "json"]);
expect(result.ok).toBe(false);
expect(renderedText(result)).toContain("schema-mismatch");
expect(renderedText(result)).toContain("must include boolean timedOut and non-negative observed fields");
});
});
});
+1
View File
@@ -371,6 +371,7 @@ export function agentRunQuery(args: string[], names: string[]): string {
const params = new URLSearchParams();
const keyMap: Record<string, string> = {
"after-seq": "afterSeq",
"timeout-ms": "timeoutMs",
"backend-profile": "backendProfile",
"command": "commandId",
"command-id": "commandId",
+5 -4
View File
@@ -59,7 +59,7 @@ export function agentRunHelp(): unknown {
"bun scripts/cli.ts agentrun get tasks -o json",
"bun scripts/cli.ts agentrun describe task/<taskId>",
"bun scripts/cli.ts agentrun describe task/<taskId> --input -o json",
"bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
"bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --expect 10 --timeout 120s --limit 100",
"bun scripts/cli.ts agentrun events run/<runId> --detail-seq <seq>",
"bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
"bun scripts/cli.ts agentrun result run/<runId> --command <commandId>",
@@ -292,9 +292,10 @@ export function agentRunHelpText(args: string[]): string {
}
if (verb === "events") {
return [
"Usage: bun scripts/cli.ts agentrun events run/<runId> [--after-seq N] [--limit 100] [-o json|yaml] [--full|--raw]",
"Usage: bun scripts/cli.ts agentrun events run/<runId> [--after-seq N] [--expect N [--timeout 120s]] [--limit 100] [-o json|yaml] [--full|--raw]",
" bun scripts/cli.ts agentrun events run/<runId> --detail-seq <seq> [-o json|yaml]",
"",
"--expect waits in one manager-held request until N new events, target completion, or timeout; --timeout defaults to 120s. --limit remains an independent output budget.",
"--detail-seq returns one bounded Secret-safe EventDetail projection. Use the separate --after-seq <seq-1> --limit 1 --full|--raw path only for explicit complete disclosure.",
].join("\n");
}
@@ -390,7 +391,7 @@ export function agentRunHelpText(args: string[]): string {
" bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
" bun scripts/cli.ts agentrun describe task/<taskId>",
" bun scripts/cli.ts agentrun describe run/<runId> --node NC01 --lane nc01-v02",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --expect 10 --timeout 120s --limit 100",
" bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
"",
"Use --raw on a resource command when you need the direct AgentRun REST envelope.",
@@ -407,7 +408,7 @@ export function agentRunHelpText(args: string[]): string {
" bun scripts/cli.ts agentrun describe task/<taskId>",
" bun scripts/cli.ts agentrun describe task/<taskId> --input -o json",
" bun scripts/cli.ts agentrun describe run/<runId> --node NC01 --lane nc01-v02",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --expect 10 --timeout 120s --limit 100",
" bun scripts/cli.ts agentrun events run/<runId> --detail-seq <seq>",
" bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
" bun scripts/cli.ts agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run",
+11
View File
@@ -360,6 +360,13 @@ export interface EventLikePageDisclosure {
hasMore: boolean;
nextAfterSeq: number | string | null;
detailCommand: string;
wait: {
expect: number;
timeoutMs: number;
timedOut: boolean;
observed: number;
completionReason: string | null;
} | null;
}
export function renderEventLike(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, kind: string, items: Record<string, unknown>[], sourceName: string, pageDisclosure: EventLikePageDisclosure | null = null): RenderedCliResult {
@@ -376,6 +383,7 @@ export function renderEventLike(command: string, raw: Record<string, unknown>, o
effectiveLimit: pageDisclosure.effectiveLimit,
fetchedCount: pageDisclosure.fetchedCount,
hasMore: pageDisclosure.hasMore,
...(pageDisclosure.wait === null ? {} : pageDisclosure.wait),
disclosure: {
output: "omitted; metadata only",
identity: "item.identity; detail --detail-seq item.seq",
@@ -415,6 +423,9 @@ export function renderEventLike(command: string, raw: Record<string, unknown>, o
])),
];
if (pageDisclosure !== null) lines.push(`Detail: ${pageDisclosure.detailCommand}`);
if (pageDisclosure?.wait !== null && pageDisclosure?.wait !== undefined) {
lines.push(`Wait: expect=${pageDisclosure.wait.expect} observed=${pageDisclosure.wait.observed} timedOut=${String(pageDisclosure.wait.timedOut)} timeoutMs=${pageDisclosure.wait.timeoutMs} nextAfterSeq=${String(nextAfterSeq ?? 0)}${pageDisclosure.wait.completionReason === null ? "" : ` reason=${pageDisclosure.wait.completionReason}`}`);
}
if (nextAfterSeq !== undefined && nextAfterSeq !== null) lines.push(`Next: bun scripts/cli.ts ${nextPagedResourceCommand(command, String(nextAfterSeq), options.limit)}`);
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
}
+70 -4
View File
@@ -178,6 +178,8 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
commandId: null,
sessionId: null,
afterSeq: null,
expect: null,
timeoutMs: 120_000,
eventDetailSeq: null,
tail: null,
fullText: false,
@@ -196,7 +198,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
mdtodoId: null,
passthroughArgs: [],
};
const valueFlags = new Set(["-o", "--output", "--limit", "--cursor", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--detail-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key", "--node", "--lane", "--target", "--target-workspace", "--repo", "--ref", "--mdtodo-id"]);
const valueFlags = new Set(["-o", "--output", "--limit", "--cursor", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--expect", "--timeout", "--detail-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key", "--node", "--lane", "--target", "--target-workspace", "--repo", "--ref", "--mdtodo-id"]);
const booleanFlags = new Set(["--full", "--raw", "--debug", "--input", "--unread", "--dry-run", "--full-text", "--prompt-stdin", "--stdin"]);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
@@ -243,7 +245,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
);
}
if (options.eventDetailSeq !== null) {
const conflictingOptions = ["--after-seq", "--limit", "--full", "--raw"]
const conflictingOptions = ["--after-seq", "--expect", "--timeout", "--limit", "--full", "--raw"]
.filter((flag) => resourceOptionPresent(args, flag));
if (conflictingOptions.length > 0) {
throw new AgentRunRestError(
@@ -253,7 +255,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
details: {
conflictingOptions: ["--detail-seq", ...conflictingOptions],
recoveryActions: [
"Use --detail-seq <seq> without --after-seq, --limit, --full, or --raw for the bounded Secret-safe EventDetail projection.",
"Use --detail-seq <seq> without --after-seq, --expect, --timeout, --limit, --full, or --raw for the bounded Secret-safe EventDetail projection.",
"Use --after-seq <seq-1> --limit 1 --full -o json or --raw only when explicitly requesting the complete durable event.",
],
valuesPrinted: false,
@@ -262,6 +264,18 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
);
}
}
if (resourceOptionPresent(args, "--timeout") && options.expect === null) {
throw new AgentRunRestError(
"validation-failed",
"--timeout requires --expect <N>; timeout controls the manager-held long poll and is not a client-side polling interval.",
{
details: {
recoveryActions: ["Use --expect <N> [--timeout <duration>] or remove --timeout for an immediate paged read."],
valuesPrinted: false,
},
},
);
}
return options;
}
@@ -278,6 +292,18 @@ function validateResourceOptionsForVerb(verb: AgentRunResourceVerb, options: Age
},
);
}
if (options.expect !== null && verb !== "events") {
throw new AgentRunRestError(
"validation-failed",
"--expect and --timeout are supported only by agentrun events run/<runId>.",
{
details: {
recoveryActions: ["Use: bun scripts/cli.ts agentrun events run/<runId> --after-seq <N> --expect <N> [--timeout 120s]."],
valuesPrinted: false,
},
},
);
}
}
function resourceOptionPresent(args: string[], flag: string): boolean {
@@ -308,6 +334,8 @@ export function applyResourceOption(options: AgentRunResourceOptions, flag: stri
else if (flag === "--command" || flag === "--command-id") options.commandId = requiredValue(value, flag);
else if (flag === "--session" || flag === "--session-id") options.sessionId = requiredValue(value, flag);
else if (flag === "--after-seq") options.afterSeq = parseNonNegativeInt(value, "--after-seq", 0, Number.MAX_SAFE_INTEGER);
else if (flag === "--expect") options.expect = parsePositiveInt(value, "--expect");
else if (flag === "--timeout") options.timeoutMs = parseDurationMs(value, "--timeout");
else if (flag === "--detail-seq") options.eventDetailSeq = parsePositiveInt(value, "--detail-seq");
else if (flag === "--tail") options.tail = parseNonNegativeInt(value, "--tail", 100, 1000);
else if (flag === "--reason") options.reason = requiredValue(value, flag);
@@ -330,6 +358,17 @@ function parsePositiveInt(raw: string | null, flag: string): number {
return value;
}
export function parseDurationMs(raw: string | null, flag: string): number {
if (raw === null || raw.length === 0) throw new Error(`${flag} requires a duration`);
const match = /^(\d+)(ms|s|m|h)$/u.exec(raw.trim());
if (match === null) throw new Error(`${flag} must use an integer duration such as 500ms, 120s, 3m, or 1h`);
const amount = Number(match[1]);
const multiplier = match[2] === "ms" ? 1 : match[2] === "s" ? 1_000 : match[2] === "m" ? 60_000 : 3_600_000;
const value = amount * multiplier;
if (!Number.isSafeInteger(value) || value < 1 || value > 86_400_000) throw new Error(`${flag} must be between 1ms and 24h`);
return value;
}
export function parseNonNegativeInt(raw: string | null, flag: string, defaultValue: number, maxValue: number): number {
if (raw === null || raw.length === 0) return defaultValue;
const value = Number(raw);
@@ -607,6 +646,7 @@ export async function resourceEvents(config: UniDeskConfig | null, command: stri
const effectiveLimit = options.full || options.raw ? requestedLimit : Math.min(requestedLimit, 20);
const requestLimit = options.full || options.raw ? effectiveLimit : Math.min(500, effectiveLimit + 1);
const eventArgs = ["events", runId, "--after-seq", String(options.afterSeq ?? 0), "--limit", String(requestLimit)];
if (options.expect !== null) eventArgs.push("--expect", String(options.expect), "--timeout-ms", String(options.timeoutMs));
const result = await runAgentRunRestCommand(config, "runs", eventArgs);
if (options.raw) return renderMachine(command, result, "json", result.ok !== false);
if (options.full) return renderMachine(command, result, options.output === "yaml" ? "yaml" : "json", result.ok !== false);
@@ -616,7 +656,32 @@ export async function resourceEvents(config: UniDeskConfig | null, command: stri
const hasMore = fetchedItems.length > visibleItems.length;
const lastVisibleSeq = visibleItems.length === 0 ? null : nonNegativeIntegerOrNull(visibleItems.at(-1)?.seq);
const responseNextAfterSeq = nonNegativeIntegerOrNull(record(innerData(result)).nextAfterSeq);
const nextAfterSeq = hasMore ? lastVisibleSeq : responseNextAfterSeq;
const nextAfterSeq = hasMore ? lastVisibleSeq : responseNextAfterSeq ?? options.afterSeq ?? 0;
const waitData = record(innerData(result));
const observed = nonNegativeIntegerOrNull(waitData.observed);
if (options.expect !== null && (typeof waitData.timedOut !== "boolean" || observed === null)) {
throw new AgentRunRestError(
"schema-mismatch",
"AgentRun manager long-poll response must include boolean timedOut and non-negative observed fields.",
{
details: {
runId,
expect: options.expect,
timeoutMs: options.timeoutMs,
returnedTimedOutType: typeof waitData.timedOut,
returnedObserved: waitData.observed ?? null,
valuesPrinted: false,
},
},
);
}
const waitDisclosure = options.expect === null ? null : {
expect: options.expect,
timeoutMs: options.timeoutMs,
timedOut: waitData.timedOut === true,
observed: observed ?? fetchedItems.length,
completionReason: stringOrNull(waitData.completionReason) ?? stringOrNull(waitData.reason),
};
const targetArgs = [
options.node === null ? null : `--node ${options.node}`,
options.lane === null ? null : `--lane ${options.lane}`,
@@ -629,6 +694,7 @@ export async function resourceEvents(config: UniDeskConfig | null, command: stri
hasMore,
nextAfterSeq,
detailCommand,
wait: waitDisclosure,
});
}
+17 -7
View File
@@ -523,7 +523,16 @@ export async function runAgentRunSessionsRest(action: string | undefined, id: st
export async function runAgentRunRunsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
if (action === "create") return await agentRunRestRequest("agentrun runs create", "POST", "/api/v1/runs", await requiredJsonBody(args, "runs create"));
if (action === "show" && id) return await agentRunRestRequest("agentrun runs show", "GET", `/api/v1/runs/${encodeURIComponent(id)}`);
if (action === "events" && id) return await agentRunRestRequest("agentrun runs events", "GET", `/api/v1/runs/${encodeURIComponent(id)}/events${agentRunQuery(args, ["after-seq", "limit"])}`);
if (action === "events" && id) {
const timeoutMs = Number(agentRunOption(args, "timeout-ms") ?? "0");
return await agentRunRestRequest(
"agentrun runs events",
"GET",
`/api/v1/runs/${encodeURIComponent(id)}/events${agentRunQuery(args, ["after-seq", "expect", "timeout-ms", "limit"])}`,
undefined,
Number.isSafeInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs + 5_000 : undefined,
);
}
if (action === "result" && id) return await agentRunRestRequest("agentrun runs result", "GET", `/api/v1/runs/${encodeURIComponent(id)}/result${agentRunQuery(args, ["command-id", "command"])}`);
if (action === "cancel" && id) return await agentRunRestRequest("agentrun runs cancel", "POST", `/api/v1/runs/${encodeURIComponent(id)}/cancel`, cancelBodyFromArgs(args));
throw new AgentRunRestError("validation-failed", `unsupported runs command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
@@ -1157,14 +1166,15 @@ export async function sessionRunnerJobBody(args: string[], defaults: Record<stri
return runnerBody;
}
export async function agentRunRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body?: unknown): Promise<Record<string, unknown>> {
export async function agentRunRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body?: unknown, requestTimeoutMs?: number): Promise<Record<string, unknown>> {
const clientConfig = readAgentRunClientConfig();
if (activeAgentRunRestTarget !== null) return await agentRunLaneRestRequest(command, method, pathValue, body, clientConfig, activeAgentRunRestTarget);
const effectiveTimeoutMs = Math.max(clientConfig.manager.timeoutMs, requestTimeoutMs ?? 0);
if (activeAgentRunRestTarget !== null) return await agentRunLaneRestRequest(command, method, pathValue, body, clientConfig, activeAgentRunRestTarget, effectiveTimeoutMs);
const auth = resolveAgentRunAuth(clientConfig);
const bridgeBase = agentRunRestBridgeMetadata(clientConfig, auth, method, pathValue);
const startedAt = Date.now();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), clientConfig.manager.timeoutMs);
const timeout = setTimeout(() => controller.abort(), effectiveTimeoutMs);
let response: Response;
try {
const headers: Record<string, string> = {
@@ -1178,7 +1188,7 @@ export async function agentRunRestRequest(command: string, method: AgentRunHttpM
response = await fetch(new URL(pathValue, clientConfig.manager.baseUrl), init);
} catch (error) {
const timedOut = isAbortLikeError(error);
throw new AgentRunRestError(timedOut ? "agentrun-timeout" : "agentrun-connect-failed", timedOut ? `AgentRun server request timed out for ${method} ${pathValue} after ${clientConfig.manager.timeoutMs}ms` : `AgentRun server connection failed for ${method} ${pathValue}: ${error instanceof Error ? error.message : String(error)}`, { bridge: { ...bridgeBase, elapsedMs: Date.now() - startedAt } });
throw new AgentRunRestError(timedOut ? "agentrun-timeout" : "agentrun-connect-failed", timedOut ? `AgentRun server request timed out for ${method} ${pathValue} after ${effectiveTimeoutMs}ms` : `AgentRun server connection failed for ${method} ${pathValue}: ${error instanceof Error ? error.message : String(error)}`, { bridge: { ...bridgeBase, elapsedMs: Date.now() - startedAt } });
} finally {
clearTimeout(timeout);
}
@@ -1215,7 +1225,7 @@ export function isAbortLikeError(error: unknown): boolean {
return error.name === "AbortError" || /abort|timed out|timeout/iu.test(error.message);
}
export async function agentRunLaneRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body: unknown, clientConfig: AgentRunClientConfig, target: AgentRunRestTarget): Promise<Record<string, unknown>> {
export async function agentRunLaneRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body: unknown, clientConfig: AgentRunClientConfig, target: AgentRunRestTarget, requestTimeoutMs = clientConfig.manager.timeoutMs): Promise<Record<string, unknown>> {
const bridgeBase = agentRunLaneRestBridgeMetadata(clientConfig, target, method, pathValue);
const startedAt = Date.now();
const proxyBaseUrl = `http://127.0.0.1:${target.spec.runtime.managerPort}`;
@@ -1225,7 +1235,7 @@ export async function agentRunLaneRestRequest(command: string, method: AgentRunH
body: body ?? null,
baseUrl: proxyBaseUrl,
serviceBaseUrl: target.spec.runtime.internalBaseUrl,
timeoutMs: clientConfig.manager.timeoutMs,
timeoutMs: requestTimeoutMs,
authEnv: clientConfig.auth.env,
header: clientConfig.auth.header,
scheme: clientConfig.auth.scheme,
+2
View File
@@ -76,6 +76,8 @@ export interface AgentRunResourceOptions {
commandId: string | null;
sessionId: string | null;
afterSeq: number | null;
expect: number | null;
timeoutMs: number;
eventDetailSeq: number | null;
tail: number | null;
fullText: boolean;