Skip to content

Commit b1df6f3

Browse files
committed
refactor(router): remove the glue chat intercept path
The chat path routed any "meta-question about the agent itself" to a cheap glue model with a "warm, no tools" system prompt. That model hallucinated identity and capability — claiming to remember things it couldn't, refusing to name its underlying model — without ever involving the real agent. Greetings and small talk now go to the main agent like every other turn. routeUserInput returns only { kind: "agent" | "plan" }; the chat variant, chatReply helper, and CHAT_SYSTEM_PROMPT are deleted. App loses its route.kind === "chat" branch. classifyIntent still returns "chat" today (cleaned up in a follow-up) — when it does, the router falls through to "agent".
1 parent af0f2ed commit b1df6f3

3 files changed

Lines changed: 32 additions & 66 deletions

File tree

src/agent/router.test.ts

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import { describe, expect, it, vi } from "vitest";
22
import type { GlueClient } from "../glue/client.js";
33
import { routeUserInput } from "./router.js";
44

5-
function mockGlue(replies: { intent?: string; chat?: string }): GlueClient {
5+
function mockGlue(replies: { intent?: string }): GlueClient {
66
const fast = vi.fn(async (_prompt: string, system?: string) => {
77
if (system?.includes("classify")) return replies.intent ?? "agent";
8-
if (system?.includes("chatting casually")) return replies.chat ?? "ok";
98
return "agent";
109
});
1110
return { fast, smart: fast } as unknown as GlueClient;
@@ -17,42 +16,26 @@ describe("routeUserInput", () => {
1716
await expect(routeUserInput(glue, "fix the build", { hasHistory: true })).resolves.toEqual({ kind: "agent" });
1817
});
1918

20-
it("returns 'chat' with a glue reply when intent classifies to chat", async () => {
21-
const glue = mockGlue({ intent: "chat", chat: "you're welcome!" });
22-
const out = await routeUserInput(glue, "thanks", { hasHistory: true });
23-
expect(out.kind).toBe("chat");
24-
if (out.kind === "chat") expect(out.reply).toBe("you're welcome!");
25-
});
26-
27-
it("short-circuits greetings to chat without an LLM intent call", async () => {
28-
const glue = mockGlue({ chat: "hello there" });
29-
const out = await routeUserInput(glue, "hi", { hasHistory: true });
30-
expect(out.kind).toBe("chat");
31-
// greeting fast-track in classifyIntent skips the intent call entirely;
32-
// only the chat-reply call to glue.fast should fire.
33-
expect(glue.fast).toHaveBeenCalledTimes(1);
34-
});
35-
36-
it("returns 'plan' when intent is plan", async () => {
19+
it("returns 'plan' when intent classifies to plan", async () => {
3720
const glue = mockGlue({ intent: "plan" });
3821
await expect(
3922
routeUserInput(glue, "rewrite the worker as a state machine", { hasHistory: false }),
40-
).resolves.toEqual({
41-
kind: "plan",
42-
});
23+
).resolves.toEqual({ kind: "plan" });
4324
});
4425

45-
it("falls back to 'chat' with a placeholder when chat reply fails", async () => {
46-
const failingGlue = {
47-
fast: vi.fn(async (_p: string, system?: string) => {
48-
if (system?.includes("classify")) return "chat";
49-
throw new Error("network");
50-
}),
51-
smart: vi.fn(),
52-
} as unknown as GlueClient;
53-
const out = await routeUserInput(failingGlue, "thanks", { hasHistory: true });
54-
expect(out.kind).toBe("chat");
55-
if (out.kind === "chat") expect(out.reply).toBe("👍");
26+
it("routes greetings to the agent (the chat-intercept path is gone)", async () => {
27+
// Greetings used to be hijacked into a glue chat reply with no tools
28+
// or context. They now go straight to the main agent like any other
29+
// turn — the system prompt teaches it to handle small talk briefly.
30+
const glue = mockGlue({ intent: "agent" });
31+
await expect(routeUserInput(glue, "hi", { hasHistory: true })).resolves.toEqual({ kind: "agent" });
32+
});
33+
34+
it("treats an unexpected intent as agent (failing open)", async () => {
35+
const glue = mockGlue({ intent: "clarify" });
36+
await expect(routeUserInput(glue, "what should I do next?", { hasHistory: true })).resolves.toEqual({
37+
kind: "agent",
38+
});
5639
});
5740

5841
it("falls back to 'agent' when intent classification errors", async () => {
@@ -62,8 +45,6 @@ describe("routeUserInput", () => {
6245
}),
6346
smart: vi.fn(),
6447
} as unknown as GlueClient;
65-
// Use a non-greeting input — greetings short-circuit to chat before the
66-
// LLM is consulted, which would change the test premise.
6748
await expect(routeUserInput(failingGlue, "fix the build", { hasHistory: true })).resolves.toEqual({
6849
kind: "agent",
6950
});

src/agent/router.ts

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,34 @@
11
import type { GlueClient } from "../glue/client.js";
22
import { classifyIntent } from "../glue/intent.js";
33

4-
const CHAT_SYSTEM_PROMPT =
5-
"You're chatting casually with the user — small talk, gratitude, greetings, meta-questions. Reply briefly (one or two sentences), warm tone, no code unless explicitly asked.";
6-
7-
export type RouteOutcome = { kind: "agent" } | { kind: "chat"; reply: string } | { kind: "plan" };
4+
/**
5+
* Routing decision returned to App.tsx. We only ever return two real
6+
* destinations now — the main agent or the plan flow. The chat path
7+
* (intercepting small-talk/meta-questions with a cheap glue model) was
8+
* removed because it made the CLI confidently hallucinate identity and
9+
* capability when the user asked the obvious meta questions. All
10+
* conversation now goes through the agent, which has tools and the
11+
* actual system prompt.
12+
*/
13+
export type RouteOutcome = { kind: "agent" } | { kind: "plan" };
814

915
export interface RouteOptions {
1016
hasHistory: boolean;
1117
signal?: AbortSignal;
1218
}
1319

1420
/**
15-
* Decide what to do with a user input before involving the main agent:
16-
* - "chat" → glue reply, no agent run (greetings, thanks, meta)
17-
* - "plan" → caller enters the plan flow (Q&A → reviewable plan → agent)
18-
* - "agent" → fall through to agent.prompt as before
19-
*
20-
* Glue failures degrade to "agent" so a flaky cheap model never silently
21-
* eats a real request — same fallback policy classifyIntent already uses.
21+
* Decide whether the user's input should auto-trigger plan mode (the
22+
* Q&A → reviewable-plan → agent-execution flow) or just hit the main
23+
* agent. Glue is consulted only for the plan/agent split — if it
24+
* returns any other intent, or the call fails, we default to the
25+
* agent. Failing-open keeps a flaky cheap model from silently eating
26+
* real requests.
2227
*/
2328
export async function routeUserInput(glue: GlueClient, text: string, options: RouteOptions): Promise<RouteOutcome> {
2429
const intent = await classifyIntent(glue, text, options);
25-
if (intent === "chat") {
26-
const reply = await chatReply(glue, text, options.signal);
27-
return { kind: "chat", reply };
28-
}
2930
if (intent === "plan") {
3031
return { kind: "plan" };
3132
}
32-
// agent + clarify both go through the main agent. A future "clarify" mode
33-
// could surface a system reminder via agent.steer() before running the turn.
3433
return { kind: "agent" };
3534
}
36-
37-
async function chatReply(glue: GlueClient, text: string, signal?: AbortSignal): Promise<string> {
38-
try {
39-
const out = await glue.fast(text, CHAT_SYSTEM_PROMPT, signal);
40-
return out.trim() || "👍";
41-
} catch {
42-
// Glue down — at least acknowledge so the input row isn't dead silent.
43-
return "👍";
44-
}
45-
}

src/ui/App.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,6 @@ function ChatApp({ initialBundle, onExit }: ChatAppProps) {
309309

310310
try {
311311
const route = await routeUserInput(bundle.glue, text, { hasHistory: hadHistory });
312-
if (route.kind === "chat") {
313-
dispatch({ type: "chat-reply", text: route.reply });
314-
return;
315-
}
316312
if (route.kind === "plan") {
317313
await runPlanFlow(bundle, text, {
318314
onReply: (replyText) => dispatch({ type: "chat-reply", text: replyText }),

0 commit comments

Comments
 (0)