Add /learn and memory
This commit is contained in:
parent
bf623a580b
commit
f35a4203d4
@ -565,6 +565,22 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
||||
},
|
||||
],
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "learn",
|
||||
nativeName: "learn",
|
||||
description: "Trigger agent to learn and remember insights from the session.",
|
||||
textAlias: "/learn",
|
||||
acceptsArgs: true,
|
||||
category: "session",
|
||||
args: [
|
||||
{
|
||||
name: "focus",
|
||||
description: "What to focus on learning (optional)",
|
||||
type: "string",
|
||||
captureRemaining: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "think",
|
||||
nativeName: "think",
|
||||
|
||||
@ -16,6 +16,7 @@ import { formatContextUsageShort, formatTokenCount } from "../status.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
import { incrementCompactionCount } from "./session-updates.js";
|
||||
import { runLearnForSession } from "./commands-learn.js";
|
||||
|
||||
function extractCompactInstructions(params: {
|
||||
rawBody?: string;
|
||||
@ -68,6 +69,38 @@ export const handleCompactCommand: CommandHandler = async (params) => {
|
||||
abortEmbeddedPiRun(sessionId);
|
||||
await waitForEmbeddedPiRunEnd(sessionId, 15_000);
|
||||
}
|
||||
|
||||
const learnResult = await runLearnForSession({
|
||||
sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
messageChannel: params.command.channel,
|
||||
groupId: params.sessionEntry.groupId,
|
||||
groupChannel: params.sessionEntry.groupChannel,
|
||||
groupSpace: params.sessionEntry.space,
|
||||
spawnedBy: params.sessionEntry.spawnedBy,
|
||||
sessionFile: resolveSessionFilePath(
|
||||
sessionId,
|
||||
params.sessionEntry,
|
||||
resolveSessionFilePathOptions({
|
||||
agentId: params.agentId,
|
||||
storePath: params.storePath,
|
||||
}),
|
||||
),
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
config: params.cfg,
|
||||
skillsSnapshot: params.sessionEntry.skillsSnapshot,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()),
|
||||
customFocus: "What insights and lessons should be remembered before context compaction?",
|
||||
senderIsOwner: params.command.senderIsOwner,
|
||||
ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined,
|
||||
});
|
||||
if (learnResult.ok) {
|
||||
logVerbose(`Pre-compaction learning completed for session ${params.sessionKey}`);
|
||||
}
|
||||
|
||||
const customInstructions = extractCompactInstructions({
|
||||
rawBody: params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body,
|
||||
ctx: params.ctx,
|
||||
|
||||
@ -13,6 +13,7 @@ import { handleApproveCommand } from "./commands-approve.js";
|
||||
import { handleBashCommand } from "./commands-bash.js";
|
||||
import { handleCompactCommand } from "./commands-compact.js";
|
||||
import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
|
||||
import { handleLearnCommand } from "./commands-learn.js";
|
||||
import {
|
||||
handleCommandsListCommand,
|
||||
handleContextCommand,
|
||||
@ -40,6 +41,11 @@ import type {
|
||||
HandleCommandsParams,
|
||||
} from "./commands-types.js";
|
||||
import { routeReply } from "./route-reply.js";
|
||||
import { runLearnForSession } from "./commands-learn.js";
|
||||
import {
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
} from "../../config/sessions.js";
|
||||
|
||||
let HANDLERS: CommandHandler[] | null = null;
|
||||
|
||||
@ -194,6 +200,7 @@ export async function handleCommands(params: HandleCommandsParams): Promise<Comm
|
||||
handleModelsCommand,
|
||||
handleStopCommand,
|
||||
handleCompactCommand,
|
||||
handleLearnCommand,
|
||||
handleAbortTrigger,
|
||||
];
|
||||
}
|
||||
@ -206,6 +213,40 @@ export async function handleCommands(params: HandleCommandsParams): Promise<Comm
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
|
||||
// Trigger learning before reset/new commands
|
||||
if (resetRequested && params.command.isAuthorizedSender && params.sessionEntry?.sessionId) {
|
||||
const learnResult = await runLearnForSession({
|
||||
sessionId: params.sessionEntry.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
messageChannel: params.command.channel,
|
||||
groupId: params.sessionEntry.groupId,
|
||||
groupChannel: params.sessionEntry.groupChannel,
|
||||
groupSpace: params.sessionEntry.space,
|
||||
spawnedBy: params.sessionEntry.spawnedBy,
|
||||
sessionFile: resolveSessionFilePath(
|
||||
params.sessionEntry.sessionId,
|
||||
params.sessionEntry,
|
||||
resolveSessionFilePathOptions({
|
||||
agentId: params.agentId,
|
||||
storePath: params.storePath,
|
||||
}),
|
||||
),
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
config: params.cfg,
|
||||
skillsSnapshot: params.sessionEntry.skillsSnapshot,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()),
|
||||
customFocus: "What insights and lessons should be remembered before starting a new session?",
|
||||
senderIsOwner: params.command.senderIsOwner,
|
||||
ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined,
|
||||
});
|
||||
if (learnResult.ok) {
|
||||
logVerbose(`Pre-reset learning completed for session ${params.sessionKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger internal hook for reset/new commands
|
||||
if (resetRequested && params.command.isAuthorizedSender) {
|
||||
const commandAction: ResetCommandAction = resetMatch?.[1] === "reset" ? "reset" : "new";
|
||||
|
||||
166
src/auto-reply/reply/commands-learn.ts
Normal file
166
src/auto-reply/reply/commands-learn.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { compactEmbeddedPiSession } from "../../agents/pi-embedded.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
} from "../../config/sessions.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
|
||||
const LEARN_SYSTEM_PROMPT = [
|
||||
"Learning turn.",
|
||||
"Analyze the session history and remember important insights.",
|
||||
].join(" ");
|
||||
|
||||
const LEARN_DEFAULT_PROMPT = [
|
||||
"Learning turn.",
|
||||
"What important insights, lessons, or information should be remembered from this session?",
|
||||
].join(" ");
|
||||
|
||||
function extractLearnFocus(params: {
|
||||
rawBody?: string;
|
||||
ctx: import("../templating.js").MsgContext;
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
isGroup: boolean;
|
||||
}): string | undefined {
|
||||
const trimmed = params.rawBody?.trim() ?? "";
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const lowered = trimmed.toLowerCase();
|
||||
const prefix = lowered.startsWith("/learn") ? "/learn" : null;
|
||||
if (!prefix) {
|
||||
return undefined;
|
||||
}
|
||||
let rest = trimmed.slice(prefix.length).trimStart();
|
||||
if (rest.startsWith(":")) {
|
||||
rest = rest.slice(1).trimStart();
|
||||
}
|
||||
return rest.length ? rest : undefined;
|
||||
}
|
||||
|
||||
export async function runLearnForSession(params: {
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
messageChannel: string;
|
||||
groupId?: string;
|
||||
groupChannel?: string;
|
||||
groupSpace?: string;
|
||||
spawnedBy?: string;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
agentDir?: string;
|
||||
config: OpenClawConfig;
|
||||
skillsSnapshot?: Record<string, unknown>;
|
||||
provider: string;
|
||||
model: string;
|
||||
thinkLevel?: string;
|
||||
customFocus?: string;
|
||||
senderIsOwner: boolean;
|
||||
ownerNumbers?: string[];
|
||||
}): Promise<{ ok: boolean; message?: string }> {
|
||||
const customInstructions = params.customFocus
|
||||
? `Focus area: ${params.customFocus}. ${LEARN_DEFAULT_PROMPT}`
|
||||
: LEARN_DEFAULT_PROMPT;
|
||||
|
||||
const result = await compactEmbeddedPiSession({
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
messageChannel: params.messageChannel,
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
sessionFile: params.sessionFile,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
config: params.config,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
thinkLevel: params.thinkLevel ?? "medium",
|
||||
bashElevated: {
|
||||
enabled: false,
|
||||
allowed: false,
|
||||
defaultLevel: "off",
|
||||
},
|
||||
customInstructions,
|
||||
extraSystemPrompt: LEARN_SYSTEM_PROMPT,
|
||||
trigger: "manual",
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
ownerNumbers: params.ownerNumbers,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
return { ok: true, message: "Learning completed. Insights saved to memory." };
|
||||
}
|
||||
return { ok: false, message: result.reason ?? "Learning failed" };
|
||||
}
|
||||
|
||||
export const handleLearnCommand = async (
|
||||
params: import("./commands-types.js").HandleCommandsParams,
|
||||
allowTextCommands: boolean,
|
||||
): Promise<import("./commands-types.js").CommandHandlerResult | null> => {
|
||||
const learnRequested =
|
||||
params.command.commandBodyNormalized === "/learn" ||
|
||||
params.command.commandBodyNormalized.startsWith("/learn ");
|
||||
if (!learnRequested) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
`Ignoring /learn from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
|
||||
if (!params.sessionEntry?.sessionId) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "Learning unavailable (missing session id)." },
|
||||
};
|
||||
}
|
||||
|
||||
const sessionId = params.sessionEntry.sessionId;
|
||||
const customFocus = extractLearnFocus({
|
||||
rawBody: params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body,
|
||||
ctx: params.ctx,
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
isGroup: params.isGroup,
|
||||
});
|
||||
|
||||
const result = await runLearnForSession({
|
||||
sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
messageChannel: params.command.channel,
|
||||
groupId: params.sessionEntry.groupId,
|
||||
groupChannel: params.sessionEntry.groupChannel,
|
||||
groupSpace: params.sessionEntry.space,
|
||||
spawnedBy: params.sessionEntry.spawnedBy,
|
||||
sessionFile: resolveSessionFilePath(
|
||||
sessionId,
|
||||
params.sessionEntry,
|
||||
resolveSessionFilePathOptions({
|
||||
agentId: params.agentId,
|
||||
storePath: params.storePath,
|
||||
}),
|
||||
),
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
config: params.cfg,
|
||||
skillsSnapshot: params.sessionEntry.skillsSnapshot,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()),
|
||||
customFocus,
|
||||
senderIsOwner: params.command.senderIsOwner,
|
||||
ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: result.ok ? `📚 ${result.message}` : `⚠️ ${result.message}` },
|
||||
};
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user