feat(consult): open the response stream before preparation, reservation and classification; rejections ride the stream (T2, BUG-1074)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
Jesse_Chen
2026-09-28 09:20:23 +08:00
co-authored by Claude Fable 5.1
parent 3f8b817261
commit 0bc6659052
8 changed files with 458 additions and 5 deletions
+33
View File
@@ -40,6 +40,7 @@ import { createServerSupabaseClient } from "@/lib/supabase/server";
import { streamTextResponse } from "@/lib/stream-text-response";
import { classifyConsultationTurn, type SmalltalkUsage } from "@/lib/consultation-smalltalk";
import { streamSmalltalkResponse } from "@/lib/stream-smalltalk-response";
import { streamFirstResponse } from "@/lib/stream-first-response";
import { consultationPublicActivityEvent, streamAgentResponse } from "@/lib/stream-agent-response";
import type { AgentExecutionReceipt, WorkflowReceipt } from "@/lib/consultation-agent-events";
import {
@@ -283,6 +284,7 @@ function workflowStatus(status: string | undefined): "ready" | "degraded" | "blo
}
export async function POST(request: Request) {
const requestStartedAt = Date.now();
let supabase: Awaited<ReturnType<typeof createServerSupabaseClient>>;
let accounting: ReturnType<typeof createAdminSupabaseClient>;
try {
@@ -406,6 +408,16 @@ export async function POST(request: Request) {
type SelectedModel = NonNullable<Awaited<ReturnType<typeof resolveSessionLanguageModel>>>;
type ReservationResult = { success: boolean; credits: number | null; error_code: string | null };
type ModelSelection = Awaited<ReturnType<typeof reserveConsultationModel<SelectedModel, ReservationResult>>>;
// Everything above answered with a real HTTP status: authentication, the
// request body, the session and its model. Everything below — subject and
// profile reads, the credit reservation, appending the question, the model
// classification, the agent itself — runs inside the response stream in the
// agentic runtime (BUG-1074): `runTurn` still returns the same Responses it
// always did, and `streamFirstResponse` relays them after a first frame that
// goes out before any of this work starts. The legacy runtime keeps the
// pre-stream order because its text/plain replies cannot ride an NDJSON stream.
const runTurn = async (): Promise<Response> => {
let prepared: PreparedConsultationRoute<ModelSelection, ConsultationPlan>;
try {
prepared = await prepareConsultationRoute<ModelSelection, ConsultationPlan>({
@@ -1625,4 +1637,25 @@ export async function POST(request: Request) {
{ status: 503 },
);
}
};
if (!shouldUseAgenticRuntime(user)) return runTurn();
return streamFirstResponse({
requestId,
run: runTurn,
startedAt: requestStartedAt,
onFirstFrame: (elapsedMs) => {
logAgentObservability({
requestId,
sessionId,
agentVersion: "consultation-stream-first-v1",
contractPhases: [{ phase: "first_byte", durationMs: elapsedMs, status: "completed" }],
billingSettlementResult: "not_applicable",
});
},
onUnhandled: (error) => {
const reason = error instanceof Error ? error.name : "UnknownError";
console.error(`[consult] turn failed before a response request=${requestId} reason=${reason}`);
},
});
}