Auto-reply: ack think directives

This commit is contained in:
Peter Steinberger
2025-12-03 08:54:38 +00:00
parent 5a83a44112
commit 48dfb1c8ca
4 changed files with 105 additions and 7 deletions

View File

@@ -53,8 +53,10 @@ function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined {
function extractThinkDirective(body?: string): {
cleaned: string;
thinkLevel?: ThinkLevel;
rawLevel?: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "" };
if (!body) return { cleaned: "", hasDirective: false };
// Match the longest keyword first to avoid partial captures (e.g. "/think:high")
const match = body.match(
/\/(?:thinking|think|t)\s*:?\s*([a-zA-Z-]+)\b/i,
@@ -63,7 +65,12 @@ function extractThinkDirective(body?: string): {
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return { cleaned, thinkLevel };
return {
cleaned,
thinkLevel,
rawLevel: match?.[1],
hasDirective: !!match,
};
}
function isAbortTrigger(text?: string): boolean {
@@ -203,9 +210,12 @@ export async function getReplyFromConfig(
IsNewSession: isNewSession ? "true" : "false",
};
const { cleaned: thinkCleaned, thinkLevel: inlineThink } = extractThinkDirective(
sessionCtx.BodyStripped ?? sessionCtx.Body ?? "",
);
const {
cleaned: thinkCleaned,
thinkLevel: inlineThink,
rawLevel: rawThinkLevel,
hasDirective: hasThinkDirective,
} = extractThinkDirective(sessionCtx.BodyStripped ?? sessionCtx.Body ?? "");
sessionCtx.Body = thinkCleaned;
sessionCtx.BodyStripped = thinkCleaned;
@@ -215,7 +225,13 @@ export async function getReplyFromConfig(
(reply?.thinkingDefault as ThinkLevel | undefined);
// Directive-only message => persist session thinking level and return ack
if (inlineThink && !thinkCleaned) {
if (hasThinkDirective && !thinkCleaned) {
if (!inlineThink) {
cleanupTyping();
return {
text: `Unrecognized thinking level "${rawThinkLevel ?? ""}". Valid levels: off, minimal, low, medium, high.`,
};
}
if (sessionEntry && sessionStore && sessionKey) {
if (inlineThink === "off") {
delete sessionEntry.thinkingLevel;
@@ -226,8 +242,12 @@ export async function getReplyFromConfig(
sessionStore[sessionKey] = sessionEntry;
await saveSessionStore(storePath, sessionStore);
}
const ack =
inlineThink === "off"
? "Thinking disabled."
: `Thinking level set to ${inlineThink}.`;
cleanupTyping();
return { text: `Thinking level set to ${inlineThink}` };
return { text: ack };
}
// Optional allowlist by origin number (E.164 without whatsapp: prefix)

View File

@@ -612,6 +612,82 @@ describe("config and templating", () => {
expect(args.join(" ")).toContain("hi there think harder");
});
it("confirms directive-only think level and skips command", async () => {
const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockResolvedValue({
stdout: "ok",
stderr: "",
code: 0,
signal: null,
killed: false,
});
const cfg = {
inbound: {
reply: {
mode: "command" as const,
command: ["echo", "{{Body}}"],
agent: { kind: "claude" },
},
},
};
const ack = await index.getReplyFromConfig(
{ Body: "/thinking high", From: "+1", To: "+2" },
undefined,
cfg,
runSpy,
);
expect(runSpy).not.toHaveBeenCalled();
expect(ack?.text).toBe("Thinking level set to high.");
});
it("rejects invalid directive-only think level without changing state", async () => {
const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockResolvedValue({
stdout: "ok",
stderr: "",
code: 0,
signal: null,
killed: false,
});
const storeDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), "warelay-session-"),
);
const storePath = path.join(storeDir, "sessions.json");
const cfg = {
inbound: {
reply: {
mode: "command" as const,
command: ["echo", "{{Body}}"],
agent: { kind: "claude" },
session: { store: storePath },
},
},
};
const ack = await index.getReplyFromConfig(
{ Body: "/thinking big", From: "+1", To: "+2" },
undefined,
cfg,
runSpy,
);
expect(runSpy).not.toHaveBeenCalled();
expect(ack?.text).toContain("Unrecognized thinking level \"big\"");
// Send another message; state should not carry any level.
const second = await index.getReplyFromConfig(
{ Body: "hi", From: "+1", To: "+2" },
undefined,
cfg,
runSpy,
);
expect(runSpy).toHaveBeenCalledTimes(1);
const args = runSpy.mock.calls[0][0] as string[];
const bodyArg = args[args.length - 1];
expect(bodyArg).toBe("hi");
expect(second?.text).toBe("ok");
});
it("uses global thinkingDefault when no directive or session override", async () => {
const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockResolvedValue({
stdout: "ok",