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.
|
- 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.
|
- 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).
|
- 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.
|
- 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).
|
- 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)
|
- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack)
|
||||||
- `/activation mention|always` (groups only)
|
- `/activation mention|always` (groups only)
|
||||||
- `/send on|off|inherit` (owner-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`)
|
- `/think <off|minimal|low|medium|high|xhigh>` (dynamic choices by model/provider; aliases: `/thinking`, `/t`)
|
||||||
- `/verbose on|full|off` (alias: `/v`)
|
- `/verbose on|full|off` (alias: `/v`)
|
||||||
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
|
- `/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:
|
Notes:
|
||||||
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
- 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`.
|
- 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.
|
- `/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.
|
- `/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 { runExec } from "../../process/exec.js";
|
||||||
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
||||||
import { buildSystemPromptParams } from "../system-prompt-params.js";
|
import { buildSystemPromptParams } from "../system-prompt-params.js";
|
||||||
|
import { resolveDefaultModelForAgent } from "../model-selection.js";
|
||||||
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
||||||
|
|
||||||
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
|
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
|
||||||
@@ -174,6 +175,11 @@ export function buildSystemPrompt(params: {
|
|||||||
modelDisplay: string;
|
modelDisplay: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const defaultModelRef = resolveDefaultModelForAgent({
|
||||||
|
cfg: params.config ?? {},
|
||||||
|
agentId: params.agentId,
|
||||||
|
});
|
||||||
|
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
||||||
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
||||||
config: params.config,
|
config: params.config,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
@@ -183,6 +189,7 @@ export function buildSystemPrompt(params: {
|
|||||||
arch: os.arch(),
|
arch: os.arch(),
|
||||||
node: process.version,
|
node: process.version,
|
||||||
model: params.modelDisplay,
|
model: params.modelDisplay,
|
||||||
|
defaultModel: defaultModelLabel,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return buildAgentSystemPrompt({
|
return buildAgentSystemPrompt({
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { ModelCatalogEntry } from "./model-catalog.js";
|
import type { ModelCatalogEntry } from "./model-catalog.js";
|
||||||
import { normalizeGoogleModelId } from "./models-config.providers.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 = {
|
export type ModelRef = {
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -141,6 +143,38 @@ export function resolveConfiguredModelRef(params: {
|
|||||||
return { provider: params.defaultProvider, model: params.defaultModel };
|
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: {
|
export function buildAllowedModelSet(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
catalog: ModelCatalogEntry[];
|
catalog: ModelCatalogEntry[];
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
resolveSkillsPromptForRun,
|
resolveSkillsPromptForRun,
|
||||||
} from "../../skills.js";
|
} from "../../skills.js";
|
||||||
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
||||||
|
import { resolveDefaultModelForAgent } from "../../model-selection.js";
|
||||||
|
|
||||||
import { isAbortError } from "../abort.js";
|
import { isAbortError } from "../abort.js";
|
||||||
import { buildEmbeddedExtensionPaths } from "../extensions.js";
|
import { buildEmbeddedExtensionPaths } from "../extensions.js";
|
||||||
@@ -212,6 +213,11 @@ export async function runEmbeddedAttempt(
|
|||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const defaultModelRef = resolveDefaultModelForAgent({
|
||||||
|
cfg: params.config ?? {},
|
||||||
|
agentId: sessionAgentId,
|
||||||
|
});
|
||||||
|
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
||||||
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
||||||
config: params.config,
|
config: params.config,
|
||||||
agentId: sessionAgentId,
|
agentId: sessionAgentId,
|
||||||
@@ -221,6 +227,7 @@ export async function runEmbeddedAttempt(
|
|||||||
arch: os.arch(),
|
arch: os.arch(),
|
||||||
node: process.version,
|
node: process.version,
|
||||||
model: `${params.provider}/${params.modelId}`,
|
model: `${params.provider}/${params.modelId}`,
|
||||||
|
defaultModel: defaultModelLabel,
|
||||||
channel: runtimeChannel,
|
channel: runtimeChannel,
|
||||||
capabilities: runtimeCapabilities,
|
capabilities: runtimeCapabilities,
|
||||||
channelActions,
|
channelActions,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type RuntimeInfoInput = {
|
|||||||
arch: string;
|
arch: string;
|
||||||
node: string;
|
node: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
defaultModel?: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
capabilities?: string[];
|
capabilities?: string[];
|
||||||
/** Supported message actions for the current channel (e.g., react, edit, unsend) */
|
/** Supported message actions for the current channel (e.g., react, edit, unsend) */
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
arch: "arm64",
|
arch: "arm64",
|
||||||
node: "v20",
|
node: "v20",
|
||||||
model: "anthropic/claude",
|
model: "anthropic/claude",
|
||||||
|
defaultModel: "anthropic/claude-opus-4-5",
|
||||||
},
|
},
|
||||||
"telegram",
|
"telegram",
|
||||||
["inlineButtons"],
|
["inlineButtons"],
|
||||||
@@ -299,6 +300,7 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
expect(line).toContain("os=macOS (arm64)");
|
expect(line).toContain("os=macOS (arm64)");
|
||||||
expect(line).toContain("node=v20");
|
expect(line).toContain("node=v20");
|
||||||
expect(line).toContain("model=anthropic/claude");
|
expect(line).toContain("model=anthropic/claude");
|
||||||
|
expect(line).toContain("default_model=anthropic/claude-opus-4-5");
|
||||||
expect(line).toContain("channel=telegram");
|
expect(line).toContain("channel=telegram");
|
||||||
expect(line).toContain("capabilities=inlineButtons");
|
expect(line).toContain("capabilities=inlineButtons");
|
||||||
expect(line).toContain("thinking=low");
|
expect(line).toContain("thinking=low");
|
||||||
|
|||||||
@@ -582,6 +582,7 @@ export function buildRuntimeLine(
|
|||||||
arch?: string;
|
arch?: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
defaultModel?: string;
|
||||||
},
|
},
|
||||||
runtimeChannel?: string,
|
runtimeChannel?: string,
|
||||||
runtimeCapabilities: string[] = [],
|
runtimeCapabilities: string[] = [],
|
||||||
@@ -597,6 +598,7 @@ export function buildRuntimeLine(
|
|||||||
: "",
|
: "",
|
||||||
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
||||||
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
||||||
|
runtimeInfo?.defaultModel ? `default_model=${runtimeInfo.defaultModel}` : "",
|
||||||
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
||||||
runtimeChannel
|
runtimeChannel
|
||||||
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`
|
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`
|
||||||
|
|||||||
@@ -312,12 +312,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "reset",
|
nativeName: "reset",
|
||||||
description: "Reset the current session.",
|
description: "Reset the current session.",
|
||||||
textAlias: "/reset",
|
textAlias: "/reset",
|
||||||
|
acceptsArgs: true,
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "new",
|
key: "new",
|
||||||
nativeName: "new",
|
nativeName: "new",
|
||||||
description: "Start a new session.",
|
description: "Start a new session.",
|
||||||
textAlias: "/new",
|
textAlias: "/new",
|
||||||
|
acceptsArgs: true,
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "compact",
|
key: "compact",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
|||||||
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
||||||
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
|
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
|
||||||
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
|
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
|
||||||
|
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
|
||||||
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
|
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
|
||||||
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
||||||
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
||||||
@@ -93,6 +94,11 @@ async function resolveContextReport(
|
|||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
});
|
});
|
||||||
|
const defaultModelRef = resolveDefaultModelForAgent({
|
||||||
|
cfg: params.cfg,
|
||||||
|
agentId: sessionAgentId,
|
||||||
|
});
|
||||||
|
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
||||||
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
agentId: sessionAgentId,
|
agentId: sessionAgentId,
|
||||||
@@ -102,6 +108,7 @@ async function resolveContextReport(
|
|||||||
arch: "unknown",
|
arch: "unknown",
|
||||||
node: process.version,
|
node: process.version,
|
||||||
model: `${params.provider}/${params.model}`,
|
model: `${params.provider}/${params.model}`,
|
||||||
|
defaultModel: defaultModelLabel,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const sandboxInfo = sandboxRuntime.sandboxed
|
const sandboxInfo = sandboxRuntime.sandboxed
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
resolveAgentDir,
|
resolveAgentDir,
|
||||||
resolveAgentModelPrimary,
|
|
||||||
resolveDefaultAgentId,
|
resolveDefaultAgentId,
|
||||||
resolveSessionAgentId,
|
resolveSessionAgentId,
|
||||||
} from "../../agents/agent-scope.js";
|
} from "../../agents/agent-scope.js";
|
||||||
import { lookupContextTokens } from "../../agents/context.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 {
|
import {
|
||||||
buildModelAliasIndex,
|
buildModelAliasIndex,
|
||||||
type ModelAliasIndex,
|
type ModelAliasIndex,
|
||||||
modelKey,
|
modelKey,
|
||||||
resolveConfiguredModelRef,
|
resolveDefaultModelForAgent,
|
||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
@@ -239,36 +238,14 @@ export function resolveDefaultModel(params: { cfg: ClawdbotConfig; agentId?: str
|
|||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
aliasIndex: ModelAliasIndex;
|
aliasIndex: ModelAliasIndex;
|
||||||
} {
|
} {
|
||||||
const agentModelOverride = params.agentId
|
const mainModel = resolveDefaultModelForAgent({
|
||||||
? resolveAgentModelPrimary(params.cfg, params.agentId)
|
cfg: params.cfg,
|
||||||
: undefined;
|
agentId: params.agentId,
|
||||||
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 defaultProvider = mainModel.provider;
|
const defaultProvider = mainModel.provider;
|
||||||
const defaultModel = mainModel.model;
|
const defaultModel = mainModel.model;
|
||||||
const aliasIndex = buildModelAliasIndex({
|
const aliasIndex = buildModelAliasIndex({
|
||||||
cfg,
|
cfg: params.cfg,
|
||||||
defaultProvider,
|
defaultProvider,
|
||||||
});
|
});
|
||||||
return { defaultProvider, defaultModel, aliasIndex };
|
return { defaultProvider, defaultModel, aliasIndex };
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
|||||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||||
import { runReplyAgent } from "./agent-runner.js";
|
import { runReplyAgent } from "./agent-runner.js";
|
||||||
import { applySessionHints } from "./body.js";
|
import { applySessionHints } from "./body.js";
|
||||||
|
import { routeReply } from "./route-reply.js";
|
||||||
import type { buildCommandContext } from "./commands.js";
|
import type { buildCommandContext } from "./commands.js";
|
||||||
import type { InlineDirectives } from "./directive-handling.js";
|
import type { InlineDirectives } from "./directive-handling.js";
|
||||||
import { buildGroupIntro } from "./groups.js";
|
import { buildGroupIntro } from "./groups.js";
|
||||||
@@ -51,7 +52,7 @@ type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
|
|||||||
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||||
|
|
||||||
const BARE_SESSION_RESET_PROMPT =
|
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 = {
|
type RunPreparedReplyParams = {
|
||||||
ctx: MsgContext;
|
ctx: MsgContext;
|
||||||
@@ -92,9 +93,11 @@ type RunPreparedReplyParams = {
|
|||||||
};
|
};
|
||||||
typing: TypingController;
|
typing: TypingController;
|
||||||
opts?: GetReplyOptions;
|
opts?: GetReplyOptions;
|
||||||
|
defaultProvider: string;
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
isNewSession: boolean;
|
isNewSession: boolean;
|
||||||
|
resetTriggered: boolean;
|
||||||
systemSent: boolean;
|
systemSent: boolean;
|
||||||
sessionEntry?: SessionEntry;
|
sessionEntry?: SessionEntry;
|
||||||
sessionStore?: Record<string, SessionEntry>;
|
sessionStore?: Record<string, SessionEntry>;
|
||||||
@@ -220,9 +223,11 @@ export async function runPreparedReply(
|
|||||||
perMessageQueueOptions,
|
perMessageQueueOptions,
|
||||||
typing,
|
typing,
|
||||||
opts,
|
opts,
|
||||||
|
defaultProvider,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
isNewSession,
|
isNewSession,
|
||||||
|
resetTriggered,
|
||||||
systemSent,
|
systemSent,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
sessionId,
|
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 sessionIdFinal = sessionId ?? crypto.randomUUID();
|
||||||
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
|
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
|
||||||
const queueBodyBase = [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n");
|
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 { runPreparedReply } from "./get-reply-run.js";
|
||||||
import { finalizeInboundContext } from "./inbound-context.js";
|
import { finalizeInboundContext } from "./inbound-context.js";
|
||||||
import { initSessionState } from "./session.js";
|
import { initSessionState } from "./session.js";
|
||||||
|
import { applyResetModelOverride } from "./session-reset-model.js";
|
||||||
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
||||||
import { createTypingController } from "./typing.js";
|
import { createTypingController } from "./typing.js";
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ export async function getReplyFromConfig(
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
sessionId,
|
sessionId,
|
||||||
isNewSession,
|
isNewSession,
|
||||||
|
resetTriggered,
|
||||||
systemSent,
|
systemSent,
|
||||||
abortedLastRun,
|
abortedLastRun,
|
||||||
storePath,
|
storePath,
|
||||||
@@ -110,8 +112,24 @@ export async function getReplyFromConfig(
|
|||||||
groupResolution,
|
groupResolution,
|
||||||
isGroup,
|
isGroup,
|
||||||
triggerBodyNormalized,
|
triggerBodyNormalized,
|
||||||
|
bodyStripped,
|
||||||
} = sessionState;
|
} = sessionState;
|
||||||
|
|
||||||
|
await applyResetModelOverride({
|
||||||
|
cfg,
|
||||||
|
resetTriggered,
|
||||||
|
bodyStripped,
|
||||||
|
sessionCtx,
|
||||||
|
ctx: finalized,
|
||||||
|
sessionEntry,
|
||||||
|
sessionStore,
|
||||||
|
sessionKey,
|
||||||
|
storePath,
|
||||||
|
defaultProvider,
|
||||||
|
defaultModel,
|
||||||
|
aliasIndex,
|
||||||
|
});
|
||||||
|
|
||||||
const directiveResult = await resolveReplyDirectives({
|
const directiveResult = await resolveReplyDirectives({
|
||||||
ctx: finalized,
|
ctx: finalized,
|
||||||
cfg,
|
cfg,
|
||||||
@@ -256,9 +274,11 @@ export async function getReplyFromConfig(
|
|||||||
perMessageQueueOptions,
|
perMessageQueueOptions,
|
||||||
typing,
|
typing,
|
||||||
opts,
|
opts,
|
||||||
|
defaultProvider,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
isNewSession,
|
isNewSession,
|
||||||
|
resetTriggered,
|
||||||
systemSent,
|
systemSent,
|
||||||
sessionEntry,
|
sessionEntry,
|
||||||
sessionStore,
|
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;
|
sessionKey: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
isNewSession: boolean;
|
isNewSession: boolean;
|
||||||
|
resetTriggered: boolean;
|
||||||
systemSent: boolean;
|
systemSent: boolean;
|
||||||
abortedLastRun: boolean;
|
abortedLastRun: boolean;
|
||||||
storePath: string;
|
storePath: string;
|
||||||
@@ -327,6 +328,7 @@ export async function initSessionState(params: {
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
sessionId: sessionId ?? crypto.randomUUID(),
|
sessionId: sessionId ?? crypto.randomUUID(),
|
||||||
isNewSession,
|
isNewSession,
|
||||||
|
resetTriggered,
|
||||||
systemSent,
|
systemSent,
|
||||||
abortedLastRun,
|
abortedLastRun,
|
||||||
storePath,
|
storePath,
|
||||||
|
|||||||
Reference in New Issue
Block a user