mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(agent-sdk): add timeout/abort support to Run.wait() and drop runFromInfo
Greptile P1: wait() polled indefinitely with no escape hatch — a stuck
or partitioned server would hang the caller forever. Now accepts
{ signal, timeoutMs, pollMs } and throws LiteLLMAgentError with codes
wait_aborted / wait_timeout. Forwards the signal to the underlying
requestJson call and to the inter-poll sleep.
Greptile P2: runFromInfo was exported but had no internal callers
(agent.ts and session.ts both build Run directly). Removed to shrink
the surface and avoid leaking resolveClient as a construction detail.
This commit is contained in:
parent
db579303fd
commit
67cad5ea60
1 changed files with 58 additions and 16 deletions
|
|
@ -4,9 +4,10 @@
|
|||
* Lifecycle: queued → running → completed | failed | cancelled.
|
||||
*/
|
||||
|
||||
import { resolveClient, requestJson, type ResolvedClient } from "./client/http.js";
|
||||
import { requestJson, type ResolvedClient } from "./client/http.js";
|
||||
import { streamRunEvents } from "./client/sse.js";
|
||||
import {
|
||||
LiteLLMAgentError,
|
||||
type ConversationTurn,
|
||||
type RunEvent,
|
||||
type RunInfo,
|
||||
|
|
@ -15,6 +16,7 @@ import {
|
|||
} from "./types.js";
|
||||
|
||||
const TERMINAL_STATES: RunStatus[] = ["completed", "failed", "cancelled"];
|
||||
const DEFAULT_WAIT_POLL_MS = 500;
|
||||
|
||||
export class Run {
|
||||
readonly id: string;
|
||||
|
|
@ -47,22 +49,50 @@ export class Run {
|
|||
}
|
||||
|
||||
/** Open an SSE stream of events for this run. */
|
||||
stream(opts: { startingSeq?: number; signal?: AbortSignal } = {}): AsyncIterable<RunEvent> {
|
||||
stream(
|
||||
opts: { startingSeq?: number; signal?: AbortSignal } = {},
|
||||
): AsyncIterable<RunEvent> {
|
||||
return streamRunEvents(this._client, this.sessionId, this.id, opts);
|
||||
}
|
||||
|
||||
/** Block until the run reaches a terminal status. */
|
||||
async wait(): Promise<RunResult> {
|
||||
/**
|
||||
* Block until the run reaches a terminal status.
|
||||
*
|
||||
* @param opts.signal Optional `AbortSignal` to bail out early. Throws
|
||||
* `LiteLLMAgentError({ code: "wait_aborted" })`.
|
||||
* @param opts.timeoutMs Optional ceiling on total wait time. Throws
|
||||
* `LiteLLMAgentError({ code: "wait_timeout" })`.
|
||||
* @param opts.pollMs Override the poll interval (default 500ms).
|
||||
*/
|
||||
async wait(
|
||||
opts: { signal?: AbortSignal; timeoutMs?: number; pollMs?: number } = {},
|
||||
): Promise<RunResult> {
|
||||
const pollMs = opts.pollMs ?? DEFAULT_WAIT_POLL_MS;
|
||||
const deadline =
|
||||
opts.timeoutMs !== undefined && opts.timeoutMs > 0
|
||||
? Date.now() + opts.timeoutMs
|
||||
: undefined;
|
||||
|
||||
while (!TERMINAL_STATES.includes(this._status)) {
|
||||
if (opts.signal?.aborted) {
|
||||
throw new LiteLLMAgentError("wait() aborted", { code: "wait_aborted" });
|
||||
}
|
||||
if (deadline !== undefined && Date.now() >= deadline) {
|
||||
throw new LiteLLMAgentError(
|
||||
`wait() timed out after ${opts.timeoutMs}ms`,
|
||||
{ code: "wait_timeout" },
|
||||
);
|
||||
}
|
||||
const info = await requestJson<RunInfo>(this._client, {
|
||||
method: "GET",
|
||||
path: `/v2/sessions/${encodeURIComponent(this.sessionId)}/runs/${encodeURIComponent(this.id)}`,
|
||||
signal: opts.signal,
|
||||
});
|
||||
this._status = info.status;
|
||||
this._result = info.result;
|
||||
this._git = info.git;
|
||||
if (TERMINAL_STATES.includes(this._status)) break;
|
||||
await sleep(500);
|
||||
await sleep(pollMs, opts.signal);
|
||||
}
|
||||
return {
|
||||
id: this.id,
|
||||
|
|
@ -74,10 +104,13 @@ export class Run {
|
|||
|
||||
/** Snapshot of the conversation up through this run. */
|
||||
async conversation(): Promise<ConversationTurn[]> {
|
||||
const data = await requestJson<{ turns: ConversationTurn[] }>(this._client, {
|
||||
method: "GET",
|
||||
path: `/v2/sessions/${encodeURIComponent(this.sessionId)}/runs/${encodeURIComponent(this.id)}/conversation`,
|
||||
});
|
||||
const data = await requestJson<{ turns: ConversationTurn[] }>(
|
||||
this._client,
|
||||
{
|
||||
method: "GET",
|
||||
path: `/v2/sessions/${encodeURIComponent(this.sessionId)}/runs/${encodeURIComponent(this.id)}/conversation`,
|
||||
},
|
||||
);
|
||||
return data.turns ?? [];
|
||||
}
|
||||
|
||||
|
|
@ -92,11 +125,20 @@ export class Run {
|
|||
}
|
||||
}
|
||||
|
||||
/** Internal helper used by SessionHandle / Agent to build a Run from a wire response. */
|
||||
export function runFromInfo(info: RunInfo, options: { apiKey?: string; baseUrl?: string }): Run {
|
||||
return new Run(info, resolveClient(options));
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) {
|
||||
reject(new LiteLLMAgentError("wait() aborted", { code: "wait_aborted" }));
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
resolve();
|
||||
}, ms);
|
||||
const onAbort = () => {
|
||||
clearTimeout(timer);
|
||||
reject(new LiteLLMAgentError("wait() aborted", { code: "wait_aborted" }));
|
||||
};
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue