414 lines
26 KiB
TypeScript
414 lines
26 KiB
TypeScript
import type { WorkbenchCommand, WorkbenchMode } from "./contracts.ts";
|
|
import { createWorkbenchKafkaRefreshHandoff, workbenchKafkaRefreshErrorPayload } from "../cloud/workbench-kafka-refresh-handoff.ts";
|
|
import { createWorkbenchKafkaRailProjectionStream } from "../cloud/workbench-kafka-rail-stream.ts";
|
|
import { workbenchKafkaSseTransportLine } from "../cloud/workbench-kafka-sse-transport.ts";
|
|
|
|
const DEFAULT_NATIVE_SESSION_LIST_LIMIT = 20;
|
|
const MAX_NATIVE_SESSION_LIST_LIMIT = 100;
|
|
|
|
export function createWorkbenchHttpApp(options: { dispatch: (command: WorkbenchCommand) => Promise<any>; snapshot?: () => Promise<{ sessions: Record<string, Record<string, unknown>>; turns: Record<string, Record<string, unknown>> }>; providerModelCatalog?: (profile: string) => Promise<Record<string, unknown>>; authorization?: string; mode?: WorkbenchMode; kafkaEventBridge?: any; close?: () => Promise<void> }) {
|
|
return {
|
|
async fetch(request: Request): Promise<Response> {
|
|
try {
|
|
const url = new URL(request.url);
|
|
if (url.pathname === "/health/live") return json(200, { ok: true, serviceId: "hwlab-workbench-api", status: "live" });
|
|
if (url.pathname === "/health/ready") return resultResponse(await options.dispatch({ operation: "health" }));
|
|
if (options.snapshot) {
|
|
if (url.pathname === "/auth/session" || url.pathname === "/auth/bootstrap") return json(200, nativeAuthSession(options.mode));
|
|
if (url.pathname === "/auth/login" && request.method === "POST") return Response.json(nativeAuthSession(options.mode), { status: 200, headers: { "set-cookie": "hwlab_session=native-test; Path=/; HttpOnly; SameSite=Lax", "cache-control": "no-store" } });
|
|
if (url.pathname === "/v1/users/me") return json(200, { ok: true, actor: { id: "usr_native", role: "user" }, authMethod: "native-test" });
|
|
if (url.pathname === "/v1/access/status") return json(200, { ok: true, status: "active", actor: { id: "usr_native", role: "user" }, nav: ["workbench.code"] });
|
|
if (url.pathname === "/v1/provider-profiles") {
|
|
const profile = options.mode === "agentrun-native" ? "gpt.pika" : "native-test";
|
|
return json(200, { ok: true, items: [{ profile, backendProfile: profile, configured: true }], count: 1, mode: options.mode ?? "native-test" });
|
|
}
|
|
const providerModelsMatch = /^\/v1\/provider-profiles\/([^/]+)\/models$/u.exec(url.pathname);
|
|
if (providerModelsMatch && request.method === "GET") {
|
|
const profile = decodeURIComponent(providerModelsMatch[1]);
|
|
if (options.providerModelCatalog) return json(200, await options.providerModelCatalog(profile));
|
|
return json(200, nativeProviderModelFallback(profile, "provider model catalog is unavailable in this native mode"));
|
|
}
|
|
if (url.pathname === "/v1/dashboard/summary") return json(200, { ok: true, status: "ready", mode: options.mode ?? "native-test" });
|
|
if (url.pathname === "/v1/caserun/cases") return json(200, { ok: true, status: "unavailable", cases: [], count: 0, mode: options.mode ?? "native-test" });
|
|
if (url.pathname === "/v1/hwpod/specs") return json(200, { ok: true, status: "unavailable", specs: [], mode: options.mode ?? "native-test" });
|
|
if (url.pathname === "/v1/hwpod-node-ops") return json(200, { ok: true, status: "unavailable", events: [], groups: [], mode: options.mode ?? "native-test" });
|
|
if (url.pathname === "/v1/web-performance" && request.method === "POST") return json(202, { ok: true, status: "accepted", persisted: false, mode: options.mode ?? "native-test" });
|
|
if (url.pathname === "/v1/workbench/events" && request.method === "GET") return nativeEventStream(options, url);
|
|
if (url.pathname === "/v1/workbench/sessions" && request.method === "GET") return nativeSessionList(options, url);
|
|
const sessionMatch = /^\/v1\/workbench\/sessions\/([^/]+)(?:\/(messages))?$/u.exec(url.pathname);
|
|
if (sessionMatch && request.method === "GET") return nativeSessionDetail(options, decodeURIComponent(sessionMatch[1]), sessionMatch[2] === "messages");
|
|
const turnMatch = /^\/v1\/workbench\/turns\/([^/]+)$/u.exec(url.pathname);
|
|
if (turnMatch && request.method === "GET") return nativeTurn(options, decodeURIComponent(turnMatch[1]));
|
|
}
|
|
if (url.pathname === "/v1/workbench/commands" && request.method === "POST") {
|
|
requireAuthorization(request, options);
|
|
const command = await bodyObject(request) as WorkbenchCommand;
|
|
return resultResponse(await options.dispatch(command), command.operation === "health" ? 200 : 202);
|
|
}
|
|
const actor = actorFrom(request, Boolean(options.snapshot));
|
|
if (url.pathname === "/v1/agent/sessions" && request.method === "POST") { requireAuthorization(request, options); return legacyResponse(await options.dispatch({ operation: "session.create", actor, params: await bodyObject(request) }), 201); }
|
|
if (url.pathname === "/v1/agent/chat" && request.method === "POST") {
|
|
requireAuthorization(request, options);
|
|
const params = await bodyObject(request);
|
|
return legacyResponse(await options.dispatch({ operation: "turn.submit", actor, params, traceId: text(params.traceId) || undefined }), 202);
|
|
}
|
|
if (url.pathname === "/v1/agent/chat/steer" && request.method === "POST") {
|
|
requireAuthorization(request, options);
|
|
const params = await bodyObject(request);
|
|
const targetTraceId = text(params.targetTraceId) || text(params.traceId);
|
|
return legacyResponse(await options.dispatch({
|
|
operation: "turn.steer",
|
|
actor,
|
|
traceId: targetTraceId,
|
|
params: { ...params, traceId: targetTraceId, targetTraceId },
|
|
}), 202);
|
|
}
|
|
if (url.pathname === "/v1/agent/chat/cancel" && request.method === "POST") {
|
|
requireAuthorization(request, options);
|
|
const params = await bodyObject(request);
|
|
return legacyResponse(await options.dispatch({ operation: "turn.cancel", actor, traceId: text(params.traceId), params }));
|
|
}
|
|
return json(404, { ok: false, error: { code: "not_found", message: "Workbench API route was not found" } });
|
|
} catch (error: any) {
|
|
return json(400, { ok: false, error: { code: error?.code ?? "workbench_http_error", message: error?.message ?? String(error) } });
|
|
}
|
|
},
|
|
close: options.close ?? (async () => {})
|
|
};
|
|
}
|
|
|
|
function nativeProviderModelFallback(profile: string, message: string) {
|
|
const defaults: Record<string, { model: string; effort: string }> = {
|
|
"gpt.pika": { model: "gpt-5.5", effort: "medium" },
|
|
"codex-api": { model: "gpt-5.5", effort: "medium" },
|
|
deepseek: { model: "deepseek-chat", effort: "medium" },
|
|
"dsflash-go": { model: "deepseek-v4-flash", effort: "xhigh" },
|
|
"minimax-m3": { model: "MiniMax-M3", effort: "medium" }
|
|
};
|
|
const selected = defaults[profile] ?? { model: profile, effort: "medium" };
|
|
return { ok: true, status: "degraded", profile, source: "profile-default", items: [{ id: selected.model, valuesPrinted: false }], count: 1, defaultModel: selected.model, reasoningEfforts: ["low", "medium", "high", "xhigh"], defaultReasoningEffort: selected.effort, warning: { code: "provider-model-catalog-unavailable", message, blocking: false, valuesPrinted: false }, valuesPrinted: false };
|
|
}
|
|
|
|
function nativeAuthSession(mode: WorkbenchMode = "native-test") {
|
|
return {
|
|
ok: true,
|
|
authenticated: true,
|
|
actor: { id: "usr_native", role: "user" },
|
|
access: { nav: { profileId: "workbench-native", allowedIds: ["workbench.code"], valuesRedacted: true } },
|
|
mode,
|
|
valuesRedacted: true
|
|
};
|
|
}
|
|
|
|
function requireAuthorization(request: Request, options: { snapshot?: unknown; authorization?: string }) {
|
|
if (options.snapshot) return;
|
|
const expected = text(options.authorization);
|
|
const actual = text(request.headers.get("authorization"));
|
|
if (!expected || actual !== expected) throw Object.assign(new Error("Workbench API internal authentication failed"), { code: "auth_required" });
|
|
}
|
|
|
|
async function nativeSessionList(options: { snapshot?: () => Promise<any>; mode?: WorkbenchMode; kafkaEventBridge?: any }, url: URL) {
|
|
const state = await requiredSnapshot(options);
|
|
const owner = text(url.searchParams.get("ownerUserId"));
|
|
const includeSessionId = text(url.searchParams.get("includeSessionId"));
|
|
const limit = boundedNativeSessionListLimit(url.searchParams.get("limit"));
|
|
const offset = nativeSessionListCursorOffset(url.searchParams.get("cursor"));
|
|
const kafkaSummary = options.mode === "agentrun-native" ? options.kafkaEventBridge?.workbenchSessionSummaries?.() : null;
|
|
const snapshotSessions = Object.values(state.sessions).filter((session: any) => text(session.status) !== "archived" && (!owner || session.ownerUserId === owner));
|
|
const snapshotBySessionId = new Map(snapshotSessions.map((session: any) => [text(session.sessionId), session]));
|
|
const kafkaSessions = (Array.isArray(kafkaSummary?.sessions) ? kafkaSummary.sessions : []).filter((summary: any) => text(summary?.status) !== "archived");
|
|
const kafkaBySessionId = new Map(kafkaSessions.map((summary: any) => [text(summary.sessionId), summary]));
|
|
const sessionSource = options.mode === "agentrun-native"
|
|
? [...snapshotSessions.map((session: any) => ({ ...session, kafkaSummary: kafkaBySessionId.get(text(session.sessionId)) ?? null })), ...kafkaSessions
|
|
.filter((summary: any) => !snapshotBySessionId.has(text(summary.sessionId)))
|
|
.map((summary: any) => ({ sessionId: text(summary.sessionId), kafkaSummary: summary }))]
|
|
: snapshotSessions.map((session: any) => ({ ...session, kafkaSummary: null }));
|
|
const allSessions = sessionSource
|
|
.map((session: any) => {
|
|
const summary = session.kafkaSummary;
|
|
const { kafkaSummary: _kafkaSummary, ...snapshot } = session;
|
|
const nativeTestPreview = options.mode === "agentrun-native" ? null : firstUserMessagePreview(session.messages) ?? (text(session.firstUserMessagePreview) || null);
|
|
return {
|
|
...snapshot,
|
|
firstUserMessagePreview: text(summary?.firstUserMessagePreview) || nativeTestPreview,
|
|
lastUserMessageAt: text(summary?.lastUserMessageAt) || text(session.lastUserMessageAt) || null,
|
|
updatedAt: text(summary?.lastUserMessageAt) || text(session.lastUserMessageAt) || text(session.createdAt) || session.updatedAt,
|
|
lastTraceId: options.mode === "agentrun-native" ? text(summary?.lastTraceId) || null : session.lastTraceId,
|
|
status: options.mode === "agentrun-native" ? text(summary?.status) || "idle" : session.status,
|
|
railAuthority: options.mode === "agentrun-native" && summary?.revision ? "hwlab.kafka.session-index" : null,
|
|
railRevision: options.mode === "agentrun-native" ? summary?.revision ?? null : null
|
|
};
|
|
})
|
|
.sort((left: any, right: any) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
|
|
let candidates = allSessions.slice(offset, offset + limit + 1);
|
|
if (includeSessionId && !candidates.some((session: any) => text(session.sessionId) === includeSessionId)) {
|
|
const included = allSessions.find((session: any) => text(session.sessionId) === includeSessionId);
|
|
if (included) candidates = [included, ...candidates];
|
|
}
|
|
const sessions = candidates.slice(0, limit);
|
|
const hasMore = offset + limit < allSessions.length;
|
|
const warnings = kafkaSummary?.warning ? [kafkaSummary.warning] : [];
|
|
return json(200, {
|
|
ok: true,
|
|
status: "ready",
|
|
sessions,
|
|
count: sessions.length,
|
|
total: allSessions.length,
|
|
cursor: offset > 0 ? nativeSessionListCursor(offset) : null,
|
|
hasMore,
|
|
nextCursor: hasMore ? nativeSessionListCursor(offset + limit) : null,
|
|
warnings,
|
|
mode: options.mode ?? "native-test"
|
|
});
|
|
}
|
|
async function nativeSessionDetail(options: { snapshot?: () => Promise<any>; mode?: WorkbenchMode }, sessionId: string, messagesOnly: boolean) {
|
|
const state = await requiredSnapshot(options);
|
|
const session = state.sessions[sessionId];
|
|
if (!session) return json(404, { ok: false, error: { code: "session_not_found", message: "native session was not found" } });
|
|
return json(200, messagesOnly ? { ok: true, sessionId, messages: session.messages ?? [], count: Array.isArray(session.messages) ? session.messages.length : 0, mode: options.mode ?? "native-test" } : { ok: true, session, mode: options.mode ?? "native-test" });
|
|
}
|
|
async function nativeTurn(options: { snapshot?: () => Promise<any>; mode?: WorkbenchMode }, traceId: string) {
|
|
const state = await requiredSnapshot(options);
|
|
const turn = state.turns[traceId];
|
|
return turn ? json(200, { ok: true, ...turn, mode: options.mode ?? "native-test" }) : json(404, { ok: false, error: { code: "turn_not_found", message: "native turn was not found" } });
|
|
}
|
|
async function requiredSnapshot(options: { snapshot?: () => Promise<any> }) { if (!options.snapshot) throw Object.assign(new Error("native projection is unavailable"), { code: "native_projection_unavailable" }); return options.snapshot(); }
|
|
async function nativeEventStream(options: { mode?: WorkbenchMode; kafkaEventBridge?: any }, url: URL) {
|
|
const mode = options.mode ?? "native-test";
|
|
const bridge = options.kafkaEventBridge;
|
|
const sessionIds = requestedSessionIds(url);
|
|
const sessionId = sessionIds.length === 1 ? sessionIds[0] : "";
|
|
const activeSessionId = text(url.searchParams.get("activeSessionId")) || sessionId;
|
|
const traceId = text(url.searchParams.get("traceId"));
|
|
if (sessionIds.length === 0 && !traceId) return json(400, { ok: false, error: { code: "workbench_realtime_scope_required", message: "sessionId or traceId is required" } });
|
|
if (activeSessionId && !sessionIds.includes(activeSessionId)) return json(400, { ok: false, error: { code: "workbench_realtime_active_scope_invalid", message: "activeSessionId must belong to the sessionId rail scope" } });
|
|
if (mode === "agentrun-native" && (bridge?.capabilities?.liveKafkaSse !== true || typeof bridge?.subscribeLiveHwlabEvents !== "function")) {
|
|
return json(503, { ok: false, error: { code: "workbench_live_kafka_unconfigured", message: "Native Workbench requires the Kafka SSE bridge" } });
|
|
}
|
|
if (mode === "agentrun-native" && bridge?.capabilities?.kafkaRefreshReplay === true) {
|
|
return nativeKafkaRefreshEventStream(bridge, sessionIds, activeSessionId, traceId, mode);
|
|
}
|
|
const encoder = new TextEncoder();
|
|
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
let unsubscribe: (() => void) | undefined;
|
|
let closed = false;
|
|
return new Response(new ReadableStream({
|
|
start(controller) {
|
|
const write = (name: string, payload: unknown, transport: any = null) => { if (!closed) controller.enqueue(encoder.encode(`event: ${name}\n${workbenchKafkaSseTransportLine(name, transport)}data: ${JSON.stringify(payload)}\n\n`)); };
|
|
write("workbench.connected", { type: "connected", status: "connected", mode, realtimeSource: mode === "agentrun-native" ? "hwlab.event.v1" : "fixture", deliverySemantics: "live-only", liveOnly: true, replay: false, filters: { sessionIds, sessionId: sessionId || null, traceId: traceId || null }, valuesPrinted: false });
|
|
if (typeof bridge?.subscribeLiveHwlabEvents === "function") unsubscribe = bridge.subscribeLiveHwlabEvents((envelope: any, transport: any) => {
|
|
if (!nativeEnvelopeMatches(envelope, sessionIds, traceId)) return;
|
|
write("hwlab.event.v1", envelope, transport);
|
|
});
|
|
void Promise.resolve(bridge?.liveReady ?? bridge?.ready).catch((error: any) => {
|
|
write("workbench.error", { type: "error", status: "blocked", error: { code: error?.code ?? "workbench_live_kafka_start_failed", message: error?.message ?? String(error) }, realtimeSource: "hwlab.event.v1", valuesRedacted: true });
|
|
closed = true;
|
|
if (heartbeat) clearInterval(heartbeat);
|
|
unsubscribe?.();
|
|
controller.close();
|
|
});
|
|
heartbeat = setInterval(() => controller.enqueue(encoder.encode(`: native-test-heartbeat ${Date.now()}\n\n`)), 10_000);
|
|
},
|
|
cancel() { closed = true; if (heartbeat) clearInterval(heartbeat); unsubscribe?.(); }
|
|
}), { status: 200, headers: { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" } });
|
|
}
|
|
|
|
function nativeKafkaRefreshEventStream(bridge: any, sessionIds: string[], activeSessionId: string, traceId: string, mode: WorkbenchMode) {
|
|
const activeSessionIds = activeSessionId ? [activeSessionId] : [];
|
|
const refreshReplay = bridge?.refreshReplay;
|
|
if (!refreshReplay || typeof bridge?.queryHwlabEventRetention !== "function") {
|
|
return json(503, { ok: false, error: { code: "workbench_kafka_refresh_unconfigured", message: "Native Workbench Kafka refresh replay requires its retention query runtime" } });
|
|
}
|
|
const encoder = new TextEncoder();
|
|
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
let handoff: ReturnType<typeof createWorkbenchKafkaRefreshHandoff> | undefined;
|
|
let railStream: ReturnType<typeof createWorkbenchKafkaRailProjectionStream> | undefined;
|
|
let closed = false;
|
|
return new Response(new ReadableStream({
|
|
start(controller) {
|
|
const write = (name: string, payload: unknown, transport: any = null) => {
|
|
if (closed) return false;
|
|
controller.enqueue(encoder.encode(`event: ${name}\n${workbenchKafkaSseTransportLine(name, transport)}data: ${JSON.stringify(payload)}\n\n`));
|
|
return true;
|
|
};
|
|
const closeWithFailure = (error: any) => {
|
|
if (closed) return;
|
|
write("workbench.error", workbenchKafkaRefreshErrorPayload(error, { sessionId: activeSessionId, traceId }));
|
|
closed = true;
|
|
if (heartbeat) clearInterval(heartbeat);
|
|
handoff?.stop("projection-failed");
|
|
railStream?.stop();
|
|
controller.close();
|
|
};
|
|
const reportRailFailure = (error: any) => {
|
|
if (closed) return false;
|
|
return write("workbench.error", nativeKafkaRailWarningPayload(error, { sessionId: activeSessionId, traceId }));
|
|
};
|
|
railStream = createWorkbenchKafkaRailProjectionStream({
|
|
bridge,
|
|
sessionIds,
|
|
deliver: (payload: unknown) => write("workbench.session.rail", payload),
|
|
onFailure: (error: any) => { reportRailFailure(error); }
|
|
});
|
|
handoff = createWorkbenchKafkaRefreshHandoff({
|
|
sessionIds: activeSessionIds,
|
|
sessionId: activeSessionId,
|
|
traceId,
|
|
liveBufferLimit: refreshReplay.liveBufferLimit,
|
|
identityWindowLimit: refreshReplay.matchedEventLimit + refreshReplay.liveBufferLimit,
|
|
subscribeLive: (listener: (envelope: any, transport: any) => void) => bridge.subscribeLiveHwlabEvents(listener),
|
|
queryRetention: async ({ signal }: { signal: AbortSignal }) => {
|
|
const result = await bridge.queryHwlabEventRetention({
|
|
sessionIds: activeSessionIds,
|
|
sessionId: activeSessionId,
|
|
traceId,
|
|
limit: refreshReplay.matchedEventLimit,
|
|
scanLimit: refreshReplay.scanLimit,
|
|
timeoutMs: refreshReplay.timeoutMs,
|
|
groupIdPrefix: refreshReplay.groupIdPrefix,
|
|
partitionKey: activeSessionId || null,
|
|
fromBeginning: true,
|
|
signal
|
|
});
|
|
console.log(JSON.stringify({
|
|
code: "workbench-kafka-refresh-query",
|
|
component: "hwlab-workbench-native",
|
|
railSessionCount: sessionIds.length,
|
|
sessionCount: activeSessionIds.length,
|
|
sessionId: activeSessionId || null,
|
|
traceId: traceId || null,
|
|
completionReason: result?.completionReason ?? null,
|
|
complete: result?.completion?.complete === true,
|
|
scannedCount: result?.scannedCount ?? null,
|
|
parsedCount: result?.parsedCount ?? null,
|
|
matchedCount: result?.matchedCount ?? null,
|
|
filterRejectedCount: result?.filterRejectedCount ?? null,
|
|
keyRejectedCount: result?.keyRejectedCount ?? null,
|
|
partitionKeyScoped: result?.partitionKeyScoped === true,
|
|
targetPartition: result?.targetPartition ?? null,
|
|
index: result?.index ?? null,
|
|
timing: result?.timing ?? null,
|
|
valuesPrinted: false
|
|
}));
|
|
return result;
|
|
},
|
|
deliverEvent: (envelope: any, transport: any) => write("hwlab.event.v1", envelope, transport),
|
|
deliverConnected: (summary: any) => write("workbench.connected", {
|
|
type: "connected",
|
|
status: "connected",
|
|
mode,
|
|
capabilities: bridge.capabilities,
|
|
realtimeSource: "hwlab.event.v1",
|
|
deliverySemantics: "kafka-retention-then-live",
|
|
liveOnly: false,
|
|
replay: true,
|
|
replaySupported: true,
|
|
lossPossible: false,
|
|
filters: { sessionIds, activeSessionId: activeSessionId || null, sessionId: activeSessionId || null, traceId: traceId || null },
|
|
railProjection: railStream?.status(),
|
|
refreshReplay: summary,
|
|
valuesPrinted: false
|
|
}),
|
|
onFailure: closeWithFailure
|
|
});
|
|
void Promise.resolve(bridge.liveReady ?? bridge.ready)
|
|
.then(() => {
|
|
void railStream?.start().catch(reportRailFailure);
|
|
return handoff?.start();
|
|
})
|
|
.then(() => {
|
|
if (closed) return;
|
|
heartbeat = setInterval(() => write("workbench.heartbeat", {
|
|
type: "heartbeat",
|
|
status: "connected",
|
|
realtimeSource: "hwlab.event.v1",
|
|
deliverySemantics: "kafka-retention-then-live",
|
|
liveOnly: false,
|
|
replay: true,
|
|
lossPossible: false,
|
|
serverSentAt: new Date().toISOString(),
|
|
valuesPrinted: false
|
|
}), 10_000);
|
|
})
|
|
.catch(closeWithFailure);
|
|
},
|
|
cancel() {
|
|
closed = true;
|
|
if (heartbeat) clearInterval(heartbeat);
|
|
handoff?.stop("connection-closed");
|
|
railStream?.stop();
|
|
}
|
|
}), { status: 200, headers: { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" } });
|
|
}
|
|
|
|
function nativeKafkaRailWarningPayload(error: any, scope: { sessionId?: string; traceId?: string } = {}) {
|
|
return {
|
|
type: "error",
|
|
status: "degraded",
|
|
phase: "session-rail",
|
|
traceId: text(scope.traceId),
|
|
sessionId: text(scope.sessionId),
|
|
error: {
|
|
code: text(error?.code) || "workbench_kafka_session_rail_unavailable",
|
|
message: text(error?.message) || "Kafka session rail projection is unavailable.",
|
|
phase: "session-rail",
|
|
retryable: true,
|
|
blocking: false,
|
|
valuesRedacted: true
|
|
},
|
|
blocking: false,
|
|
fallback: false,
|
|
valuesRedacted: true
|
|
};
|
|
}
|
|
|
|
function nativeEnvelopeMatches(envelope: any, sessionIds: string[], traceId: string) {
|
|
const event = envelope?.event && typeof envelope.event === "object" ? envelope.event : {};
|
|
const envelopeSessionId = text(envelope?.sessionId ?? envelope?.hwlabSessionId ?? event.sessionId);
|
|
const envelopeTraceId = text(envelope?.traceId ?? event.traceId);
|
|
const targetTraceId = text(envelope?.targetTraceId ?? event.targetTraceId ?? envelope?.context?.targetTraceId);
|
|
return (sessionIds.length === 0 || sessionIds.includes(envelopeSessionId)) && (!traceId || envelopeTraceId === traceId || targetTraceId === traceId);
|
|
}
|
|
|
|
function requestedSessionIds(url: URL): string[] {
|
|
return [...new Set(url.searchParams.getAll("sessionId").map(text).filter(Boolean))].sort();
|
|
}
|
|
|
|
function boundedNativeSessionListLimit(value: unknown): number {
|
|
const parsed = Number.parseInt(text(value), 10);
|
|
return Math.min(MAX_NATIVE_SESSION_LIST_LIMIT, Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_NATIVE_SESSION_LIST_LIMIT);
|
|
}
|
|
|
|
function nativeSessionListCursorOffset(value: unknown): number {
|
|
const raw = text(value);
|
|
const parsed = Number.parseInt(raw.startsWith("idx:") ? raw.slice(4) : raw, 10);
|
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
}
|
|
|
|
function nativeSessionListCursor(offset: number): string {
|
|
return `idx:${Math.max(0, Math.trunc(offset))}`;
|
|
}
|
|
|
|
function firstUserMessagePreview(messages: unknown): string | null {
|
|
if (!Array.isArray(messages)) return null;
|
|
for (const message of messages) {
|
|
if (!message || typeof message !== "object" || text((message as any).role) !== "user") continue;
|
|
const preview = text((message as any).text ?? (message as any).content ?? (message as any).message);
|
|
if (preview) return preview.slice(0, 240);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function actorFrom(request: Request, nativeTest: boolean) {
|
|
const id = text(request.headers.get("x-hwlab-actor-id"));
|
|
if (!id && nativeTest) return { id: "usr_native", role: "user" };
|
|
if (!id) throw Object.assign(new Error("x-hwlab-actor-id is required"), { code: "auth_required" });
|
|
return { id, role: text(request.headers.get("x-hwlab-actor-role")) || "user" };
|
|
}
|
|
function resultResponse(result: any, successStatus = 200) { return json(result?.ok === true ? successStatus : statusFor(result?.error?.code), result); }
|
|
function legacyResponse(result: any, successStatus = 200) { return result?.ok === true ? json(successStatus, { ok: true, ...result.data, orchestrationMode: result.mode }) : resultResponse(result, successStatus); }
|
|
function statusFor(code: unknown) { const value = String(code ?? ""); if (value === "auth_required") return 401; if (value.includes("not_found")) return 404; if (value.includes("required") || value === "invalid_input") return 400; return 503; }
|
|
async function bodyObject(request: Request) { const body = await request.json().catch(() => null); if (!body || typeof body !== "object" || Array.isArray(body)) throw Object.assign(new Error("JSON object body is required"), { code: "invalid_json" }); return body as Record<string, unknown>; }
|
|
function text(value: unknown) { return String(value ?? "").trim(); }
|
|
function json(status: number, body: unknown) { return Response.json(body, { status, headers: { "cache-control": "no-store" } }); }
|