296 lines
8.3 KiB
TypeScript
296 lines
8.3 KiB
TypeScript
import { getChannelDock } from "../../channels/dock.js";
|
|
import type { SkillCommandSpec } from "../../agents/skills.js";
|
|
import type { ClawdbotConfig } from "../../config/config.js";
|
|
import type { SessionEntry } from "../../config/sessions.js";
|
|
import type { MsgContext, TemplateContext } from "../templating.js";
|
|
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
|
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
|
import { getAbortMemory } from "./abort.js";
|
|
import { buildStatusReply, handleCommands } from "./commands.js";
|
|
import type { InlineDirectives } from "./directive-handling.js";
|
|
import { isDirectiveOnly } from "./directive-handling.js";
|
|
import type { createModelSelectionState } from "./model-selection.js";
|
|
import { extractInlineSimpleCommand } from "./reply-inline.js";
|
|
import type { TypingController } from "./typing.js";
|
|
import { listSkillCommandsForWorkspace, resolveSkillCommandInvocation } from "../skill-commands.js";
|
|
import { logVerbose } from "../../globals.js";
|
|
|
|
export type InlineActionResult =
|
|
| { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
|
|
| {
|
|
kind: "continue";
|
|
directives: InlineDirectives;
|
|
abortedLastRun: boolean;
|
|
};
|
|
|
|
export async function handleInlineActions(params: {
|
|
ctx: MsgContext;
|
|
sessionCtx: TemplateContext;
|
|
cfg: ClawdbotConfig;
|
|
agentId: string;
|
|
sessionEntry?: SessionEntry;
|
|
sessionStore?: Record<string, SessionEntry>;
|
|
sessionKey: string;
|
|
storePath?: string;
|
|
sessionScope: Parameters<typeof buildStatusReply>[0]["sessionScope"];
|
|
workspaceDir: string;
|
|
isGroup: boolean;
|
|
opts?: GetReplyOptions;
|
|
typing: TypingController;
|
|
allowTextCommands: boolean;
|
|
inlineStatusRequested: boolean;
|
|
command: Parameters<typeof handleCommands>[0]["command"];
|
|
skillCommands?: SkillCommandSpec[];
|
|
directives: InlineDirectives;
|
|
cleanedBody: string;
|
|
elevatedEnabled: boolean;
|
|
elevatedAllowed: boolean;
|
|
elevatedFailures: Array<{ gate: string; key: string }>;
|
|
defaultActivation: Parameters<typeof buildStatusReply>[0]["defaultGroupActivation"];
|
|
resolvedThinkLevel: ThinkLevel | undefined;
|
|
resolvedVerboseLevel: VerboseLevel | undefined;
|
|
resolvedReasoningLevel: ReasoningLevel;
|
|
resolvedElevatedLevel: ElevatedLevel;
|
|
resolveDefaultThinkingLevel: Awaited<
|
|
ReturnType<typeof createModelSelectionState>
|
|
>["resolveDefaultThinkingLevel"];
|
|
provider: string;
|
|
model: string;
|
|
contextTokens: number;
|
|
directiveAck?: ReplyPayload;
|
|
abortedLastRun: boolean;
|
|
skillFilter?: string[];
|
|
}): Promise<InlineActionResult> {
|
|
const {
|
|
ctx,
|
|
sessionCtx,
|
|
cfg,
|
|
agentId,
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
sessionScope,
|
|
workspaceDir,
|
|
isGroup,
|
|
opts,
|
|
typing,
|
|
allowTextCommands,
|
|
inlineStatusRequested,
|
|
command,
|
|
directives: initialDirectives,
|
|
cleanedBody: initialCleanedBody,
|
|
elevatedEnabled,
|
|
elevatedAllowed,
|
|
elevatedFailures,
|
|
defaultActivation,
|
|
resolvedThinkLevel,
|
|
resolvedVerboseLevel,
|
|
resolvedReasoningLevel,
|
|
resolvedElevatedLevel,
|
|
resolveDefaultThinkingLevel,
|
|
provider,
|
|
model,
|
|
contextTokens,
|
|
directiveAck,
|
|
abortedLastRun: initialAbortedLastRun,
|
|
skillFilter,
|
|
} = params;
|
|
|
|
let directives = initialDirectives;
|
|
let cleanedBody = initialCleanedBody;
|
|
|
|
const shouldLoadSkillCommands = command.commandBodyNormalized.startsWith("/");
|
|
const skillCommands =
|
|
shouldLoadSkillCommands && params.skillCommands
|
|
? params.skillCommands
|
|
: shouldLoadSkillCommands
|
|
? listSkillCommandsForWorkspace({
|
|
workspaceDir,
|
|
cfg,
|
|
skillFilter,
|
|
})
|
|
: [];
|
|
|
|
const skillInvocation =
|
|
allowTextCommands && skillCommands.length > 0
|
|
? resolveSkillCommandInvocation({
|
|
commandBodyNormalized: command.commandBodyNormalized,
|
|
skillCommands,
|
|
})
|
|
: null;
|
|
if (skillInvocation) {
|
|
if (!command.isAuthorizedSender) {
|
|
logVerbose(
|
|
`Ignoring /${skillInvocation.command.name} from unauthorized sender: ${command.senderId || "<unknown>"}`,
|
|
);
|
|
typing.cleanup();
|
|
return { kind: "reply", reply: undefined };
|
|
}
|
|
const promptParts = [
|
|
`Use the "${skillInvocation.command.skillName}" skill for this request.`,
|
|
skillInvocation.args ? `User input:\n${skillInvocation.args}` : null,
|
|
].filter((entry): entry is string => Boolean(entry));
|
|
const rewrittenBody = promptParts.join("\n\n");
|
|
ctx.Body = rewrittenBody;
|
|
sessionCtx.Body = rewrittenBody;
|
|
sessionCtx.BodyStripped = rewrittenBody;
|
|
cleanedBody = rewrittenBody;
|
|
}
|
|
|
|
const sendInlineReply = async (reply?: ReplyPayload) => {
|
|
if (!reply) return;
|
|
if (!opts?.onBlockReply) return;
|
|
await opts.onBlockReply(reply);
|
|
};
|
|
|
|
const inlineCommand =
|
|
allowTextCommands && command.isAuthorizedSender
|
|
? extractInlineSimpleCommand(cleanedBody)
|
|
: null;
|
|
if (inlineCommand) {
|
|
cleanedBody = inlineCommand.cleaned;
|
|
sessionCtx.Body = cleanedBody;
|
|
sessionCtx.BodyStripped = cleanedBody;
|
|
}
|
|
|
|
const handleInlineStatus =
|
|
!isDirectiveOnly({
|
|
directives,
|
|
cleanedBody: directives.cleaned,
|
|
ctx,
|
|
cfg,
|
|
agentId,
|
|
isGroup,
|
|
}) && inlineStatusRequested;
|
|
if (handleInlineStatus) {
|
|
const inlineStatusReply = await buildStatusReply({
|
|
cfg,
|
|
command,
|
|
sessionEntry,
|
|
sessionKey,
|
|
sessionScope,
|
|
provider,
|
|
model,
|
|
contextTokens,
|
|
resolvedThinkLevel,
|
|
resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
|
|
resolvedReasoningLevel,
|
|
resolvedElevatedLevel,
|
|
resolveDefaultThinkingLevel,
|
|
isGroup,
|
|
defaultGroupActivation: defaultActivation,
|
|
});
|
|
await sendInlineReply(inlineStatusReply);
|
|
directives = { ...directives, hasStatusDirective: false };
|
|
}
|
|
|
|
if (inlineCommand) {
|
|
const inlineCommandContext = {
|
|
...command,
|
|
rawBodyNormalized: inlineCommand.command,
|
|
commandBodyNormalized: inlineCommand.command,
|
|
};
|
|
const inlineResult = await handleCommands({
|
|
ctx,
|
|
cfg,
|
|
command: inlineCommandContext,
|
|
agentId,
|
|
directives,
|
|
elevated: {
|
|
enabled: elevatedEnabled,
|
|
allowed: elevatedAllowed,
|
|
failures: elevatedFailures,
|
|
},
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
sessionScope,
|
|
workspaceDir,
|
|
defaultGroupActivation: defaultActivation,
|
|
resolvedThinkLevel,
|
|
resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
|
|
resolvedReasoningLevel,
|
|
resolvedElevatedLevel,
|
|
resolveDefaultThinkingLevel,
|
|
provider,
|
|
model,
|
|
contextTokens,
|
|
isGroup,
|
|
skillCommands,
|
|
});
|
|
if (inlineResult.reply) {
|
|
if (!inlineCommand.cleaned) {
|
|
typing.cleanup();
|
|
return { kind: "reply", reply: inlineResult.reply };
|
|
}
|
|
await sendInlineReply(inlineResult.reply);
|
|
}
|
|
}
|
|
|
|
if (directiveAck) {
|
|
await sendInlineReply(directiveAck);
|
|
}
|
|
|
|
const isEmptyConfig = Object.keys(cfg).length === 0;
|
|
const skipWhenConfigEmpty = command.channelId
|
|
? Boolean(getChannelDock(command.channelId)?.commands?.skipWhenConfigEmpty)
|
|
: false;
|
|
if (
|
|
skipWhenConfigEmpty &&
|
|
isEmptyConfig &&
|
|
command.from &&
|
|
command.to &&
|
|
command.from !== command.to
|
|
) {
|
|
typing.cleanup();
|
|
return { kind: "reply", reply: undefined };
|
|
}
|
|
|
|
let abortedLastRun = initialAbortedLastRun;
|
|
if (!sessionEntry && command.abortKey) {
|
|
abortedLastRun = getAbortMemory(command.abortKey) ?? false;
|
|
}
|
|
|
|
const commandResult = await handleCommands({
|
|
ctx,
|
|
cfg,
|
|
command,
|
|
agentId,
|
|
directives,
|
|
elevated: {
|
|
enabled: elevatedEnabled,
|
|
allowed: elevatedAllowed,
|
|
failures: elevatedFailures,
|
|
},
|
|
sessionEntry,
|
|
sessionStore,
|
|
sessionKey,
|
|
storePath,
|
|
sessionScope,
|
|
workspaceDir,
|
|
defaultGroupActivation: defaultActivation,
|
|
resolvedThinkLevel,
|
|
resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
|
|
resolvedReasoningLevel,
|
|
resolvedElevatedLevel,
|
|
resolveDefaultThinkingLevel,
|
|
provider,
|
|
model,
|
|
contextTokens,
|
|
isGroup,
|
|
skillCommands,
|
|
});
|
|
if (!commandResult.shouldContinue) {
|
|
typing.cleanup();
|
|
return { kind: "reply", reply: commandResult.reply };
|
|
}
|
|
|
|
return {
|
|
kind: "continue",
|
|
directives,
|
|
abortedLastRun,
|
|
};
|
|
}
|