Promote AgentCompany-New to the repository root and retire the old app
The legacy application - its src, tests, landing site, promo assets, docs, release tooling, CI workflows, and root configs - leaves the tree; it stays reachable in history. Everything under AgentCompany-New moves up unchanged: same package name, so the production userData/database path is untouched, and e2e/build scripts already resolved paths relative to the project. The developer cred.txt stays gitignored at its new location. Full verify:ci passes from the new root.
This commit is contained in:
1 parent
2350f744e3
commit
07d0b4587d
1904 files changed
+1133
-282075
No files matched your search
@@ -0,0 +1,229 @@
|
||||
import type { ProviderObservedRateLimitState } from "../../shared/provider-runtime-domain";
|
||||
import { parseProviderRateLimitHeaders } from "./provider-rate-limit-registry";
|
||||
|
||||
export type JsonRecord = Record<string, unknown>;
|
||||
|
||||
export interface ProviderJsonResponse {
|
||||
data: JsonRecord;
|
||||
rateLimitState: ProviderObservedRateLimitState | null;
|
||||
}
|
||||
|
||||
export interface ProviderEventStreamMessage {
|
||||
eventName: string | null;
|
||||
data: JsonRecord;
|
||||
rawData: string;
|
||||
}
|
||||
|
||||
export interface ProviderEventStreamResponse {
|
||||
messages: AsyncIterable<ProviderEventStreamMessage>;
|
||||
rateLimitState: ProviderObservedRateLimitState | null;
|
||||
}
|
||||
|
||||
const HEADERS_TIMEOUT_MS = 60_000;
|
||||
const BODY_TIMEOUT_MS = 300_000;
|
||||
const STREAM_IDLE_TIMEOUT_MS = 60_000;
|
||||
|
||||
export interface ProviderRequestTimeouts {
|
||||
headersTimeoutMs?: number;
|
||||
bodyTimeoutMs?: number;
|
||||
idleTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export class ProviderHttpError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly rateLimitState: ProviderObservedRateLimitState | null,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ProviderHttpError";
|
||||
}
|
||||
}
|
||||
|
||||
export function providerRateLimitStateFromError(error: unknown): ProviderObservedRateLimitState | null {
|
||||
return error instanceof ProviderHttpError ? error.rateLimitState : null;
|
||||
}
|
||||
|
||||
export async function postJson(
|
||||
endpoint: string,
|
||||
headers: Record<string, string>,
|
||||
body: JsonRecord,
|
||||
timeouts: ProviderRequestTimeouts = {},
|
||||
): Promise<ProviderJsonResponse> {
|
||||
return requestJson(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}, timeouts);
|
||||
}
|
||||
|
||||
export async function getJson(endpoint: string, headers: Record<string, string>): Promise<ProviderJsonResponse> {
|
||||
return requestJson(endpoint, { method: "GET", headers });
|
||||
}
|
||||
|
||||
export async function postEventStream(
|
||||
endpoint: string,
|
||||
headers: Record<string, string>,
|
||||
body: JsonRecord,
|
||||
timeouts: ProviderRequestTimeouts = {},
|
||||
): Promise<ProviderEventStreamResponse> {
|
||||
const controller = new AbortController();
|
||||
const response = await withDeadline(controller, timeouts.headersTimeoutMs ?? HEADERS_TIMEOUT_MS, () => fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "text/event-stream",
|
||||
"content-type": "application/json",
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
}));
|
||||
const rateLimitState = parseProviderRateLimitHeaders(response.headers);
|
||||
if (!response.ok) {
|
||||
const raw = await withDeadline(controller, timeouts.bodyTimeoutMs ?? BODY_TIMEOUT_MS, () => response.text());
|
||||
const data = parseProviderJson(raw, false);
|
||||
const message = readProviderError(data) ?? (raw.trim() || `Provider request failed with HTTP ${response.status}.`);
|
||||
throw new ProviderHttpError(message, rateLimitState);
|
||||
}
|
||||
if (!response.body) throw new ProviderHttpError("Provider returned an empty event stream.", rateLimitState);
|
||||
return { messages: readServerSentEvents(response.body, timeouts.idleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS), rateLimitState };
|
||||
}
|
||||
|
||||
async function requestJson(endpoint: string, init: RequestInit, timeouts: ProviderRequestTimeouts = {}): Promise<ProviderJsonResponse> {
|
||||
const controller = new AbortController();
|
||||
const response = await withDeadline(controller, timeouts.headersTimeoutMs ?? HEADERS_TIMEOUT_MS, () => fetch(endpoint, { ...init, signal: controller.signal }));
|
||||
const rateLimitState = parseProviderRateLimitHeaders(response.headers);
|
||||
const raw = await withDeadline(controller, timeouts.bodyTimeoutMs ?? BODY_TIMEOUT_MS, () => response.text());
|
||||
const data = parseProviderJson(raw, response.ok);
|
||||
if (!response.ok) {
|
||||
const message = readProviderError(data) ?? (raw.trim() || `Provider request failed with HTTP ${response.status}.`);
|
||||
throw new ProviderHttpError(message, rateLimitState);
|
||||
}
|
||||
return { data, rateLimitState };
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one phase of a provider request under a hard deadline. The abort
|
||||
* controller covers the WHOLE request — a server that returns headers and
|
||||
* then stalls the body must not hang the caller, so the body read gets its
|
||||
* own (longer) deadline after the headers arrive.
|
||||
*/
|
||||
async function withDeadline<T>(controller: AbortController, timeoutMs: number, operation: () => Promise<T>): Promise<T> {
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) throw new ProviderHttpError("Provider request timed out.", null);
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
export async function* readServerSentEvents(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
idleTimeoutMs: number = STREAM_IDLE_TIMEOUT_MS,
|
||||
): AsyncIterable<ProviderEventStreamMessage> {
|
||||
const decoder = new TextDecoder();
|
||||
const state: EventStreamParseState = { eventName: null, dataLines: [] };
|
||||
let lineBuffer = "";
|
||||
const reader = body.getReader();
|
||||
try {
|
||||
for (;;) {
|
||||
const { value, done } = await readWithIdleDeadline(reader, idleTimeoutMs);
|
||||
if (done) break;
|
||||
lineBuffer += decoder.decode(value, { stream: true });
|
||||
const lines = lineBuffer.split("\n");
|
||||
lineBuffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
const message = parseServerSentEventLine(state, line);
|
||||
if (message) yield message;
|
||||
}
|
||||
}
|
||||
lineBuffer += decoder.decode();
|
||||
if (lineBuffer.length > 0) {
|
||||
const message = parseServerSentEventLine(state, lineBuffer);
|
||||
if (message) yield message;
|
||||
}
|
||||
const message = flushServerSentEvent(state);
|
||||
if (message) yield message;
|
||||
} catch (error) {
|
||||
await reader.cancel().catch(() => undefined);
|
||||
throw error;
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
/** A healthy event stream emits chunks continuously; a read that produces nothing within the idle window means the connection is dead. */
|
||||
async function readWithIdleDeadline(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
idleTimeoutMs: number,
|
||||
): Promise<ReadableStreamReadResult<Uint8Array>> {
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
const idle = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => reject(new ProviderHttpError("Provider stream timed out.", null)), idleTimeoutMs);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([reader.read(), idle]);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function parseServerSentEventLine(state: EventStreamParseState, rawLine: string): ProviderEventStreamMessage | null {
|
||||
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
||||
if (line === "") return flushServerSentEvent(state);
|
||||
if (line.startsWith(":")) return null;
|
||||
const separatorIndex = line.indexOf(":");
|
||||
const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
||||
const rawValue = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
||||
const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
|
||||
if (field === "event") state.eventName = value;
|
||||
if (field === "data") state.dataLines.push(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
function flushServerSentEvent(state: EventStreamParseState): ProviderEventStreamMessage | null {
|
||||
if (state.dataLines.length === 0) {
|
||||
state.eventName = null;
|
||||
return null;
|
||||
}
|
||||
const rawData = state.dataLines.join("\n");
|
||||
const eventName = state.eventName;
|
||||
state.eventName = null;
|
||||
state.dataLines = [];
|
||||
if (rawData === "[DONE]") return null;
|
||||
return { eventName, data: parseProviderJson(rawData, true), rawData };
|
||||
}
|
||||
|
||||
function parseProviderJson(raw: string, requireJson: boolean): JsonRecord {
|
||||
if (!raw.trim()) return {};
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(raw) as unknown;
|
||||
} catch (error) {
|
||||
if (!requireJson) return {};
|
||||
throw error;
|
||||
}
|
||||
if (!isRecord(value)) throw new Error("Provider returned invalid JSON.");
|
||||
return value;
|
||||
}
|
||||
|
||||
function readProviderError(data: JsonRecord): string | null {
|
||||
const error = data.error;
|
||||
if (typeof error === "string") return error;
|
||||
if (isRecord(error) && typeof error.message === "string") return error.message;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isRecord(value: unknown): value is JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
interface EventStreamParseState {
|
||||
eventName: string | null;
|
||||
dataLines: string[];
|
||||
}
|
||||
Reference in new issue
Block a user