fix(stop+stability): stop button interrupts live generation; classifier, prompt + preview pane improvements
Stop button fix: - Plumb AbortSignal end-to-end: callVibnChat → Gemini SDK (config.abortSignal) / OpenAI fetch → executeMcpTool (/api/mcp fetch) - Treat abort as clean user stop (not fatal error); partial reply persisted with '(stopped by user)' Classifier fix: - Add timeout/gateway/5xx/connection-error vocabulary to diagnose intent - Prevents 'I get a gateway timeout' from falling through to feature_build (40 rounds) and looping Prompt / agent behaviour: - Render verification is now scope-aware: small edits stop at green healthCheck; no browser_console/curl audit on healthy server - Sanitize stale '### Phase Checkpoint' walls from loaded history so old threads stop biasing new turns - Next.js dev command updated to --no-turbopack for container stability (per-route lazy compile caused cold-start 503s) - New public page prompt: agent checks middleware allowlist in the same turn - Scope discipline and QA-tool gating carried forward from prior session Code cleanup: - Remove duplicate AgentPhase declaration (TS2440) - Remove dead checkpoint emit branch and orphan 'checkpoint' phase value - Remove unused MAX_TOOL_ROUNDS constant Preview pane (build status): - 4-state machine: initial-load / building (with elapsed timer) / build-failed / not-running - pollMs 0 → 5 000ms so dev-server recovery and build completion auto-update without refresh - anatomy route + use-anatomy type: inFlightBuild gains createdAt for elapsed timer
This commit is contained in:
@@ -109,6 +109,8 @@ export async function callGeminiChat(opts: {
|
||||
tools?: ToolDefinition[];
|
||||
temperature?: number;
|
||||
includeThoughts?: boolean;
|
||||
/** Cancels the in-flight generation when the user clicks Stop. */
|
||||
signal?: AbortSignal;
|
||||
}): Promise<{
|
||||
text: string;
|
||||
thoughts: string;
|
||||
@@ -129,6 +131,10 @@ export async function callGeminiChat(opts: {
|
||||
const fns = toGeminiFunctions(opts.tools ?? []);
|
||||
if (fns) config.tools = fns;
|
||||
|
||||
// Forward the request abort signal so a Stop click cancels the actual
|
||||
// Gemini generation instead of only short-circuiting the next round.
|
||||
if (opts.signal) config.abortSignal = opts.signal;
|
||||
|
||||
console.log("\n========================================================");
|
||||
console.log("➡️ [GEMINI API REQUEST]");
|
||||
console.log("========================================================");
|
||||
@@ -227,11 +233,16 @@ export async function callGeminiChat(opts: {
|
||||
finishReason: response.candidates?.[0]?.finishReason,
|
||||
};
|
||||
} catch (error) {
|
||||
const aborted =
|
||||
opts.signal?.aborted ||
|
||||
(error instanceof Error && error.name === "AbortError");
|
||||
return {
|
||||
text: "",
|
||||
thoughts: "",
|
||||
toolCalls: [],
|
||||
error: `GoogleGenAI error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error: aborted
|
||||
? "aborted"
|
||||
: `GoogleGenAI error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +257,8 @@ export async function callOpenAiCompatibleChat(opts: {
|
||||
temperature?: number;
|
||||
/** Unused for OpenAI-compat; kept for call-site symmetry */
|
||||
includeThoughts?: boolean;
|
||||
/** Cancels the in-flight request when the user clicks Stop. */
|
||||
signal?: AbortSignal;
|
||||
}): Promise<{
|
||||
text: string;
|
||||
thoughts: string;
|
||||
@@ -330,13 +332,18 @@ export async function callOpenAiCompatibleChat(opts: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: opts.signal,
|
||||
});
|
||||
} catch (e) {
|
||||
const aborted =
|
||||
opts.signal?.aborted || (e instanceof Error && e.name === "AbortError");
|
||||
return {
|
||||
text: "",
|
||||
thoughts: "",
|
||||
toolCalls: [],
|
||||
error: `Network error: ${e instanceof Error ? e.message : String(e)}`,
|
||||
error: aborted
|
||||
? "aborted"
|
||||
: `Network error: ${e instanceof Error ? e.message : String(e)}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
* Optional: VIBN_OPENAI_COMPATIBLE_MODEL (default deepseek-chat)
|
||||
*/
|
||||
|
||||
import type { ChatMessage, ToolDefinition } from './gemini-chat';
|
||||
import { callGeminiChat } from './gemini-chat';
|
||||
import { callOpenAiCompatibleChat } from './openai-compatible-chat';
|
||||
import type { ChatMessage, ToolDefinition } from "./gemini-chat";
|
||||
import { callGeminiChat } from "./gemini-chat";
|
||||
import { callOpenAiCompatibleChat } from "./openai-compatible-chat";
|
||||
|
||||
export type VibnChatCallOpts = {
|
||||
systemPrompt: string;
|
||||
@@ -22,11 +22,13 @@ export type VibnChatCallOpts = {
|
||||
tools?: ToolDefinition[];
|
||||
temperature?: number;
|
||||
includeThoughts?: boolean;
|
||||
/** Forwarded to the provider so a Stop click cancels the live request. */
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
export async function callVibnChat(opts: VibnChatCallOpts) {
|
||||
const p = (process.env.VIBN_CHAT_PROVIDER || 'gemini').toLowerCase().trim();
|
||||
if (p === 'deepseek' || p === 'openai_compatible') {
|
||||
const p = (process.env.VIBN_CHAT_PROVIDER || "gemini").toLowerCase().trim();
|
||||
if (p === "deepseek" || p === "openai_compatible") {
|
||||
return callOpenAiCompatibleChat(opts);
|
||||
}
|
||||
return callGeminiChat(opts);
|
||||
|
||||
@@ -12,13 +12,7 @@ import type { ToolDefinition } from "./gemini-chat";
|
||||
|
||||
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || "";
|
||||
|
||||
export type AgentPhase =
|
||||
| "plan"
|
||||
| "recon"
|
||||
| "checkpoint"
|
||||
| "execute"
|
||||
| "verify"
|
||||
| "final";
|
||||
export type AgentPhase = "plan" | "recon" | "execute" | "verify" | "final";
|
||||
|
||||
export type TurnIntent =
|
||||
| "conversational"
|
||||
@@ -77,7 +71,12 @@ export function filterToolsForPhase(
|
||||
phase: AgentPhase,
|
||||
intent: TurnIntent,
|
||||
): ToolDefinition[] {
|
||||
if (phase === "recon" || phase === "verify") {
|
||||
// `verify` stays read-only: when the agent is checking its work it should
|
||||
// inspect, not start new edits. `recon` and `execute` both expose the full
|
||||
// toolset so the agent can read AND edit directly in one fluid turn — the
|
||||
// old read-only `recon` paired with the (now-removed) forced checkpoint to
|
||||
// gate editing, which made the agent plan and stall instead of acting.
|
||||
if (phase === "verify") {
|
||||
return tools.filter(
|
||||
(t) =>
|
||||
READ_ONLY_TOOLS.has(t.name) ||
|
||||
@@ -85,10 +84,7 @@ export function filterToolsForPhase(
|
||||
t.name === "browser_navigate",
|
||||
);
|
||||
}
|
||||
if (phase === "execute") {
|
||||
return tools; // All tools allowed
|
||||
}
|
||||
return tools; // Default fallback
|
||||
return tools; // recon + execute: all tools allowed
|
||||
}
|
||||
|
||||
export const VIBN_TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||
@@ -1887,6 +1883,7 @@ export async function executeMcpTool(
|
||||
mcpToken: string,
|
||||
baseUrl: string,
|
||||
projectId?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
if (toolName === "github_search") return executeGithubSearch(args);
|
||||
if (toolName === "github_file") return executeGithubFile(args);
|
||||
@@ -1923,6 +1920,7 @@ export async function executeMcpTool(
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ action, params }),
|
||||
signal,
|
||||
});
|
||||
const data = await res.json();
|
||||
return JSON.stringify(data.result ?? data.error ?? data, null, 2).slice(
|
||||
@@ -1930,8 +1928,14 @@ export async function executeMcpTool(
|
||||
8000,
|
||||
);
|
||||
} catch (e) {
|
||||
const aborted =
|
||||
signal?.aborted || (e instanceof Error && e.name === "AbortError");
|
||||
return JSON.stringify({
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
error: aborted
|
||||
? "aborted by user"
|
||||
: e instanceof Error
|
||||
? e.message
|
||||
: String(e),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user