feat: improve /new model hints and reset confirmation
This commit is contained in:
@@ -60,7 +60,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
|
||||
- Idle reset (optional): `idleMinutes` adds a sliding idle window. When both daily and idle resets are configured, **whichever expires first** forces a new session.
|
||||
- Legacy idle-only: if you set `session.idleMinutes` without any `session.reset`/`resetByType` config, Clawdbot stays in idle-only mode for backward compatibility.
|
||||
- Per-type overrides (optional): `resetByType` lets you override the policy for `dm`, `group`, and `thread` sessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector).
|
||||
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset.
|
||||
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. `/new <model>` accepts a model alias, `provider/model`, or provider name (fuzzy match) to set the new session model. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset.
|
||||
- Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
|
||||
- Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse).
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ Text + native (when enabled):
|
||||
- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack)
|
||||
- `/activation mention|always` (groups only)
|
||||
- `/send on|off|inherit` (owner-only)
|
||||
- `/reset` or `/new`
|
||||
- `/reset` or `/new [model]` (optional model hint; remainder is passed through)
|
||||
- `/think <off|minimal|low|medium|high|xhigh>` (dynamic choices by model/provider; aliases: `/thinking`, `/t`)
|
||||
- `/verbose on|full|off` (alias: `/v`)
|
||||
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
|
||||
@@ -91,6 +91,7 @@ Text-only:
|
||||
|
||||
Notes:
|
||||
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
||||
- `/new <model>` accepts a model alias, `provider/model`, or a provider name (fuzzy match); if no match, the text is treated as the message body.
|
||||
- For full provider usage breakdown, use `clawdbot status --usage`.
|
||||
- `/usage` controls the per-response usage footer; `/usage cost` prints a local cost summary from Clawdbot session logs.
|
||||
- `/restart` is disabled by default; set `commands.restart: true` to enable it.
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { CliBackendConfig } from "../../config/types.js";
|
||||
import { runExec } from "../../process/exec.js";
|
||||
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
||||
import { buildSystemPromptParams } from "../system-prompt-params.js";
|
||||
import { resolveDefaultModelForAgent } from "../model-selection.js";
|
||||
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
||||
|
||||
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
|
||||
@@ -174,6 +175,11 @@ export function buildSystemPrompt(params: {
|
||||
modelDisplay: string;
|
||||
agentId?: string;
|
||||
}) {
|
||||
const defaultModelRef = resolveDefaultModelForAgent({
|
||||
cfg: params.config ?? {},
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
||||
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
||||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
@@ -183,6 +189,7 @@ export function buildSystemPrompt(params: {
|
||||
arch: os.arch(),
|
||||
node: process.version,
|
||||
model: params.modelDisplay,
|
||||
defaultModel: defaultModelLabel,
|
||||
},
|
||||
});
|
||||
return buildAgentSystemPrompt({
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { ModelCatalogEntry } from "./model-catalog.js";
|
||||
import { normalizeGoogleModelId } from "./models-config.providers.js";
|
||||
import { resolveAgentModelPrimary } from "./agent-scope.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||
|
||||
export type ModelRef = {
|
||||
provider: string;
|
||||
@@ -141,6 +143,38 @@ export function resolveConfiguredModelRef(params: {
|
||||
return { provider: params.defaultProvider, model: params.defaultModel };
|
||||
}
|
||||
|
||||
export function resolveDefaultModelForAgent(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
agentId?: string;
|
||||
}): ModelRef {
|
||||
const agentModelOverride = params.agentId
|
||||
? resolveAgentModelPrimary(params.cfg, params.agentId)
|
||||
: undefined;
|
||||
const cfg =
|
||||
agentModelOverride && agentModelOverride.length > 0
|
||||
? {
|
||||
...params.cfg,
|
||||
agents: {
|
||||
...params.cfg.agents,
|
||||
defaults: {
|
||||
...params.cfg.agents?.defaults,
|
||||
model: {
|
||||
...(typeof params.cfg.agents?.defaults?.model === "object"
|
||||
? params.cfg.agents.defaults.model
|
||||
: undefined),
|
||||
primary: agentModelOverride,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: params.cfg;
|
||||
return resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildAllowedModelSet(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
catalog: ModelCatalogEntry[];
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
resolveSkillsPromptForRun,
|
||||
} from "../../skills.js";
|
||||
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
||||
import { resolveDefaultModelForAgent } from "../../model-selection.js";
|
||||
|
||||
import { isAbortError } from "../abort.js";
|
||||
import { buildEmbeddedExtensionPaths } from "../extensions.js";
|
||||
@@ -212,6 +213,11 @@ export async function runEmbeddedAttempt(
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const defaultModelRef = resolveDefaultModelForAgent({
|
||||
cfg: params.config ?? {},
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
||||
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
@@ -221,6 +227,7 @@ export async function runEmbeddedAttempt(
|
||||
arch: os.arch(),
|
||||
node: process.version,
|
||||
model: `${params.provider}/${params.modelId}`,
|
||||
defaultModel: defaultModelLabel,
|
||||
channel: runtimeChannel,
|
||||
capabilities: runtimeCapabilities,
|
||||
channelActions,
|
||||
|
||||
@@ -13,6 +13,7 @@ export type RuntimeInfoInput = {
|
||||
arch: string;
|
||||
node: string;
|
||||
model: string;
|
||||
defaultModel?: string;
|
||||
channel?: string;
|
||||
capabilities?: string[];
|
||||
/** Supported message actions for the current channel (e.g., react, edit, unsend) */
|
||||
|
||||
@@ -288,6 +288,7 @@ describe("buildAgentSystemPrompt", () => {
|
||||
arch: "arm64",
|
||||
node: "v20",
|
||||
model: "anthropic/claude",
|
||||
defaultModel: "anthropic/claude-opus-4-5",
|
||||
},
|
||||
"telegram",
|
||||
["inlineButtons"],
|
||||
@@ -299,6 +300,7 @@ describe("buildAgentSystemPrompt", () => {
|
||||
expect(line).toContain("os=macOS (arm64)");
|
||||
expect(line).toContain("node=v20");
|
||||
expect(line).toContain("model=anthropic/claude");
|
||||
expect(line).toContain("default_model=anthropic/claude-opus-4-5");
|
||||
expect(line).toContain("channel=telegram");
|
||||
expect(line).toContain("capabilities=inlineButtons");
|
||||
expect(line).toContain("thinking=low");
|
||||
|
||||
@@ -582,6 +582,7 @@ export function buildRuntimeLine(
|
||||
arch?: string;
|
||||
node?: string;
|
||||
model?: string;
|
||||
defaultModel?: string;
|
||||
},
|
||||
runtimeChannel?: string,
|
||||
runtimeCapabilities: string[] = [],
|
||||
@@ -597,6 +598,7 @@ export function buildRuntimeLine(
|
||||
: "",
|
||||
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
||||
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
||||
runtimeInfo?.defaultModel ? `default_model=${runtimeInfo.defaultModel}` : "",
|
||||
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
||||
runtimeChannel
|
||||
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`
|
||||
|
||||
@@ -312,12 +312,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
||||
nativeName: "reset",
|
||||
description: "Reset the current session.",
|
||||
textAlias: "/reset",
|
||||
acceptsArgs: true,
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "new",
|
||||
nativeName: "new",
|
||||
description: "Start a new session.",
|
||||
textAlias: "/new",
|
||||
acceptsArgs: true,
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "compact",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
||||
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
||||
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
|
||||
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
|
||||
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
|
||||
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
|
||||
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
||||
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
||||
@@ -93,6 +94,11 @@ async function resolveContextReport(
|
||||
sessionKey: params.sessionKey,
|
||||
config: params.cfg,
|
||||
});
|
||||
const defaultModelRef = resolveDefaultModelForAgent({
|
||||
cfg: params.cfg,
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
||||
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
||||
config: params.cfg,
|
||||
agentId: sessionAgentId,
|
||||
@@ -102,6 +108,7 @@ async function resolveContextReport(
|
||||
arch: "unknown",
|
||||
node: process.version,
|
||||
model: `${params.provider}/${params.model}`,
|
||||
defaultModel: defaultModelLabel,
|
||||
},
|
||||
});
|
||||
const sandboxInfo = sandboxRuntime.sandboxed
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentModelPrimary,
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
type ModelAliasIndex,
|
||||
modelKey,
|
||||
resolveConfiguredModelRef,
|
||||
resolveDefaultModelForAgent,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
@@ -239,36 +238,14 @@ export function resolveDefaultModel(params: { cfg: ClawdbotConfig; agentId?: str
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
} {
|
||||
const agentModelOverride = params.agentId
|
||||
? resolveAgentModelPrimary(params.cfg, params.agentId)
|
||||
: undefined;
|
||||
const cfg =
|
||||
agentModelOverride && agentModelOverride.length > 0
|
||||
? {
|
||||
...params.cfg,
|
||||
agents: {
|
||||
...params.cfg.agents,
|
||||
defaults: {
|
||||
...params.cfg.agents?.defaults,
|
||||
model: {
|
||||
...(typeof params.cfg.agents?.defaults?.model === "object"
|
||||
? params.cfg.agents.defaults.model
|
||||
: undefined),
|
||||
primary: agentModelOverride,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: params.cfg;
|
||||
const mainModel = resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
const mainModel = resolveDefaultModelForAgent({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const defaultProvider = mainModel.provider;
|
||||
const defaultModel = mainModel.model;
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg,
|
||||
cfg: params.cfg,
|
||||
defaultProvider,
|
||||
});
|
||||
return { defaultProvider, defaultModel, aliasIndex };
|
||||
|
||||
@@ -38,6 +38,7 @@ import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import { runReplyAgent } from "./agent-runner.js";
|
||||
import { applySessionHints } from "./body.js";
|
||||
import { routeReply } from "./route-reply.js";
|
||||
import type { buildCommandContext } from "./commands.js";
|
||||
import type { InlineDirectives } from "./directive-handling.js";
|
||||
import { buildGroupIntro } from "./groups.js";
|
||||
@@ -51,7 +52,7 @@ type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
|
||||
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||
|
||||
const BARE_SESSION_RESET_PROMPT =
|
||||
"A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning.";
|
||||
"A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. If the runtime model differs from default_model in the system prompt, mention the default model in the greeting. Do not mention internal steps, files, tools, or reasoning.";
|
||||
|
||||
type RunPreparedReplyParams = {
|
||||
ctx: MsgContext;
|
||||
@@ -92,9 +93,11 @@ type RunPreparedReplyParams = {
|
||||
};
|
||||
typing: TypingController;
|
||||
opts?: GetReplyOptions;
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
timeoutMs: number;
|
||||
isNewSession: boolean;
|
||||
resetTriggered: boolean;
|
||||
systemSent: boolean;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
@@ -220,9 +223,11 @@ export async function runPreparedReply(
|
||||
perMessageQueueOptions,
|
||||
typing,
|
||||
opts,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
timeoutMs,
|
||||
isNewSession,
|
||||
resetTriggered,
|
||||
systemSent,
|
||||
sessionKey,
|
||||
sessionId,
|
||||
@@ -369,6 +374,27 @@ export async function runPreparedReply(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resetTriggered && command.isAuthorizedSender) {
|
||||
const channel = ctx.OriginatingChannel || (command.channel as any);
|
||||
const to = ctx.OriginatingTo || command.from || command.to;
|
||||
if (channel && to) {
|
||||
const modelLabel = `${provider}/${model}`;
|
||||
const defaultLabel = `${defaultProvider}/${defaultModel}`;
|
||||
const text =
|
||||
modelLabel === defaultLabel
|
||||
? `✅ New session started · model: ${modelLabel}`
|
||||
: `✅ New session started · model: ${modelLabel} (default: ${defaultLabel})`;
|
||||
await routeReply({
|
||||
payload: { text },
|
||||
channel,
|
||||
to,
|
||||
sessionKey,
|
||||
accountId: ctx.AccountId,
|
||||
threadId: ctx.MessageThreadId,
|
||||
cfg,
|
||||
});
|
||||
}
|
||||
}
|
||||
const sessionIdFinal = sessionId ?? crypto.randomUUID();
|
||||
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
|
||||
const queueBodyBase = [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n");
|
||||
|
||||
@@ -19,6 +19,7 @@ import { handleInlineActions } from "./get-reply-inline-actions.js";
|
||||
import { runPreparedReply } from "./get-reply-run.js";
|
||||
import { finalizeInboundContext } from "./inbound-context.js";
|
||||
import { initSessionState } from "./session.js";
|
||||
import { applyResetModelOverride } from "./session-reset-model.js";
|
||||
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
||||
import { createTypingController } from "./typing.js";
|
||||
|
||||
@@ -103,6 +104,7 @@ export async function getReplyFromConfig(
|
||||
sessionKey,
|
||||
sessionId,
|
||||
isNewSession,
|
||||
resetTriggered,
|
||||
systemSent,
|
||||
abortedLastRun,
|
||||
storePath,
|
||||
@@ -110,8 +112,24 @@ export async function getReplyFromConfig(
|
||||
groupResolution,
|
||||
isGroup,
|
||||
triggerBodyNormalized,
|
||||
bodyStripped,
|
||||
} = sessionState;
|
||||
|
||||
await applyResetModelOverride({
|
||||
cfg,
|
||||
resetTriggered,
|
||||
bodyStripped,
|
||||
sessionCtx,
|
||||
ctx: finalized,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
storePath,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
aliasIndex,
|
||||
});
|
||||
|
||||
const directiveResult = await resolveReplyDirectives({
|
||||
ctx: finalized,
|
||||
cfg,
|
||||
@@ -256,9 +274,11 @@ export async function getReplyFromConfig(
|
||||
perMessageQueueOptions,
|
||||
typing,
|
||||
opts,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
timeoutMs,
|
||||
isNewSession,
|
||||
resetTriggered,
|
||||
systemSent,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
|
||||
74
src/auto-reply/reply/session-reset-model.test.ts
Normal file
74
src/auto-reply/reply/session-reset-model.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { buildModelAliasIndex } from "../../agents/model-selection.js";
|
||||
import { applyResetModelOverride } from "./session-reset-model.js";
|
||||
|
||||
vi.mock("../../agents/model-catalog.js", () => ({
|
||||
loadModelCatalog: vi.fn(async () => [
|
||||
{ provider: "minimax", id: "m2.1", name: "M2.1" },
|
||||
{ provider: "openai", id: "gpt-4o-mini", name: "GPT-4o mini" },
|
||||
]),
|
||||
}));
|
||||
|
||||
describe("applyResetModelOverride", () => {
|
||||
it("selects a model hint and strips it from the body", async () => {
|
||||
const cfg = {} as ClawdbotConfig;
|
||||
const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: "openai" });
|
||||
const sessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
||||
const sessionCtx = { BodyStripped: "minimax summarize" };
|
||||
const ctx = { ChatType: "direct" };
|
||||
|
||||
await applyResetModelOverride({
|
||||
cfg,
|
||||
resetTriggered: true,
|
||||
bodyStripped: "minimax summarize",
|
||||
sessionCtx,
|
||||
ctx,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:dm:1",
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-4o-mini",
|
||||
aliasIndex,
|
||||
});
|
||||
|
||||
expect(sessionEntry.providerOverride).toBe("minimax");
|
||||
expect(sessionEntry.modelOverride).toBe("m2.1");
|
||||
expect(sessionCtx.BodyStripped).toBe("summarize");
|
||||
});
|
||||
|
||||
it("skips when resetTriggered is false", async () => {
|
||||
const cfg = {} as ClawdbotConfig;
|
||||
const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: "openai" });
|
||||
const sessionEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
||||
const sessionCtx = { BodyStripped: "minimax summarize" };
|
||||
const ctx = { ChatType: "direct" };
|
||||
|
||||
await applyResetModelOverride({
|
||||
cfg,
|
||||
resetTriggered: false,
|
||||
bodyStripped: "minimax summarize",
|
||||
sessionCtx,
|
||||
ctx,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: "agent:main:dm:1",
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-4o-mini",
|
||||
aliasIndex,
|
||||
});
|
||||
|
||||
expect(sessionEntry.providerOverride).toBeUndefined();
|
||||
expect(sessionEntry.modelOverride).toBeUndefined();
|
||||
expect(sessionCtx.BodyStripped).toBe("minimax summarize");
|
||||
});
|
||||
});
|
||||
191
src/auto-reply/reply/session-reset-model.ts
Normal file
191
src/auto-reply/reply/session-reset-model.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { loadModelCatalog } from "../../agents/model-catalog.js";
|
||||
import {
|
||||
buildAllowedModelSet,
|
||||
modelKey,
|
||||
normalizeProviderId,
|
||||
resolveModelRefFromString,
|
||||
type ModelAliasIndex,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { updateSessionStore } from "../../config/sessions.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import { formatInboundBodyWithSenderMeta } from "./inbound-sender-meta.js";
|
||||
import { resolveModelDirectiveSelection, type ModelDirectiveSelection } from "./model-selection.js";
|
||||
|
||||
type ResetModelResult = {
|
||||
selection?: ModelDirectiveSelection;
|
||||
cleanedBody?: string;
|
||||
};
|
||||
|
||||
function splitBody(body: string) {
|
||||
const tokens = body.split(/\s+/).filter(Boolean);
|
||||
return {
|
||||
tokens,
|
||||
first: tokens[0],
|
||||
second: tokens[1],
|
||||
rest: tokens.slice(2),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSelectionFromExplicit(params: {
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
allowedModelKeys: Set<string>;
|
||||
}): ModelDirectiveSelection | undefined {
|
||||
const resolved = resolveModelRefFromString({
|
||||
raw: params.raw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex: params.aliasIndex,
|
||||
});
|
||||
if (!resolved) return undefined;
|
||||
const key = modelKey(resolved.ref.provider, resolved.ref.model);
|
||||
if (params.allowedModelKeys.size > 0 && !params.allowedModelKeys.has(key)) return undefined;
|
||||
const isDefault =
|
||||
resolved.ref.provider === params.defaultProvider && resolved.ref.model === params.defaultModel;
|
||||
return {
|
||||
provider: resolved.ref.provider,
|
||||
model: resolved.ref.model,
|
||||
isDefault,
|
||||
...(resolved.alias ? { alias: resolved.alias } : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
function applySelectionToSession(params: {
|
||||
selection: ModelDirectiveSelection;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
storePath?: string;
|
||||
}) {
|
||||
const { selection, sessionEntry, sessionStore, sessionKey, storePath } = params;
|
||||
if (!sessionEntry || !sessionStore || !sessionKey) return;
|
||||
let updated = false;
|
||||
if (selection.isDefault) {
|
||||
if (sessionEntry.providerOverride || sessionEntry.modelOverride) {
|
||||
delete sessionEntry.providerOverride;
|
||||
delete sessionEntry.modelOverride;
|
||||
updated = true;
|
||||
}
|
||||
} else {
|
||||
if (sessionEntry.providerOverride !== selection.provider) {
|
||||
sessionEntry.providerOverride = selection.provider;
|
||||
updated = true;
|
||||
}
|
||||
if (sessionEntry.modelOverride !== selection.model) {
|
||||
sessionEntry.modelOverride = selection.model;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (!updated) return;
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
updateSessionStore(storePath, (store) => {
|
||||
store[sessionKey] = sessionEntry;
|
||||
}).catch(() => {
|
||||
// Ignore persistence errors; session still proceeds.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyResetModelOverride(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
resetTriggered: boolean;
|
||||
bodyStripped?: string;
|
||||
sessionCtx: TemplateContext;
|
||||
ctx: MsgContext;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
storePath?: string;
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
}): Promise<ResetModelResult> {
|
||||
if (!params.resetTriggered) return {};
|
||||
const rawBody = params.bodyStripped?.trim();
|
||||
if (!rawBody) return {};
|
||||
|
||||
const { tokens, first, second } = splitBody(rawBody);
|
||||
if (!first) return {};
|
||||
|
||||
const catalog = await loadModelCatalog({ config: params.cfg });
|
||||
const allowed = buildAllowedModelSet({
|
||||
cfg: params.cfg,
|
||||
catalog,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
});
|
||||
const allowedModelKeys = allowed.allowedKeys;
|
||||
if (allowedModelKeys.size === 0) return {};
|
||||
|
||||
const providers = new Set<string>();
|
||||
for (const key of allowedModelKeys) {
|
||||
const slash = key.indexOf("/");
|
||||
if (slash <= 0) continue;
|
||||
providers.add(normalizeProviderId(key.slice(0, slash)));
|
||||
}
|
||||
|
||||
const resolveSelection = (raw: string) =>
|
||||
resolveModelDirectiveSelection({
|
||||
raw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
aliasIndex: params.aliasIndex,
|
||||
allowedModelKeys,
|
||||
});
|
||||
|
||||
let selection: ModelDirectiveSelection | undefined;
|
||||
let consumed = 0;
|
||||
|
||||
if (providers.has(normalizeProviderId(first)) && second) {
|
||||
const composite = `${normalizeProviderId(first)}/${second}`;
|
||||
const resolved = resolveSelection(composite);
|
||||
if (resolved.selection) {
|
||||
selection = resolved.selection;
|
||||
consumed = 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selection) {
|
||||
selection = buildSelectionFromExplicit({
|
||||
raw: first,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
aliasIndex: params.aliasIndex,
|
||||
allowedModelKeys,
|
||||
});
|
||||
if (selection) consumed = 1;
|
||||
}
|
||||
|
||||
if (!selection) {
|
||||
const resolved = resolveSelection(first);
|
||||
const allowFuzzy = providers.has(normalizeProviderId(first)) || first.trim().length >= 6;
|
||||
if (allowFuzzy) {
|
||||
selection = resolved.selection;
|
||||
if (selection) consumed = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selection) return {};
|
||||
|
||||
const cleanedBody = tokens.slice(consumed).join(" ").trim();
|
||||
params.sessionCtx.BodyStripped = formatInboundBodyWithSenderMeta({
|
||||
ctx: params.ctx,
|
||||
body: cleanedBody,
|
||||
});
|
||||
params.sessionCtx.BodyForCommands = cleanedBody;
|
||||
|
||||
applySelectionToSession({
|
||||
selection,
|
||||
sessionEntry: params.sessionEntry,
|
||||
sessionStore: params.sessionStore,
|
||||
sessionKey: params.sessionKey,
|
||||
storePath: params.storePath,
|
||||
});
|
||||
|
||||
return { selection, cleanedBody };
|
||||
}
|
||||
@@ -40,6 +40,7 @@ export type SessionInitResult = {
|
||||
sessionKey: string;
|
||||
sessionId: string;
|
||||
isNewSession: boolean;
|
||||
resetTriggered: boolean;
|
||||
systemSent: boolean;
|
||||
abortedLastRun: boolean;
|
||||
storePath: string;
|
||||
@@ -327,6 +328,7 @@ export async function initSessionState(params: {
|
||||
sessionKey,
|
||||
sessionId: sessionId ?? crypto.randomUUID(),
|
||||
isNewSession,
|
||||
resetTriggered,
|
||||
systemSent,
|
||||
abortedLastRun,
|
||||
storePath,
|
||||
|
||||
Reference in New Issue
Block a user