openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
190 lines (189 loc) • 7.3 kB
JavaScript
import { t as jsonResult } from "./tool-results-BCM3fdVS.js";
import "./memory-core-host-runtime-core-5HBGxdZe.js";
import { c as encodeStandingIntentChannelScope, d as listStandingIntents, l as encodeStandingIntentSenderScope, n as DEFAULT_INTENT_EXPIRY_MS, o as cancelStandingIntent, s as createStandingIntent, t as DEFAULT_INTENT_COOLDOWN_SECONDS } from "./standing-intents-Igp3Bg9j.js";
//#region extensions/memory-core/src/standing-intents-tool.ts
const INTENT_DESCRIPTION_MAX_CHARS = 500;
const INTENT_KEYWORD_MAX_COUNT = 24;
const INTENT_KEYWORD_MAX_CHARS = 120;
const STANDING_INTENT_AUTOMATION_GUIDANCE = "The system injects the reminder automatically when it triggers. Do not deliver it early or cancel it unless the user asks.";
const STANDING_INTENT_SCOPE_GUIDANCE = "Use \"channel\" (the default) for any \"whenever I mention X\" request. Use \"conversation\" only when the user explicitly limits the reminder to the current thread. Use \"anywhere\" when the user asks for it everywhere.";
function trimRequiredString(value, field, maxChars) {
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required`);
const trimmed = value.trim();
if (trimmed.length > maxChars) throw new Error(`${field} must be at most ${maxChars} characters`);
return trimmed;
}
function renderArmedIntentMessage(scope) {
return `Intent is armed ${scope === "channel" ? "for this channel" : scope === "conversation" ? "for this conversation" : "everywhere"}. ${STANDING_INTENT_AUTOMATION_GUIDANCE}`;
}
function positiveInteger(value, field, fallback) {
if (value === void 0) return fallback;
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new Error(`${field} must be a positive integer`);
return value;
}
function nonNegativeInteger(value, field, fallback) {
if (value === void 0) return fallback;
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${field} must be a non-negative integer`);
return value;
}
function normalizeKeywords(value) {
if (!Array.isArray(value) || value.length === 0) throw new Error("triggerKeywords must be a non-empty string array");
const normalized = value.map((entry) => trimRequiredString(entry, "triggerKeywords entry", INTENT_KEYWORD_MAX_CHARS).toLowerCase());
const unique = [...new Set(normalized)];
if (unique.length > INTENT_KEYWORD_MAX_COUNT) throw new Error(`triggerKeywords must contain at most ${INTENT_KEYWORD_MAX_COUNT} entries`);
return unique;
}
function parseExpiry(value, nowMs) {
if (value === void 0) return nowMs + DEFAULT_INTENT_EXPIRY_MS;
if (typeof value !== "string") throw new Error("expiresAt must be an ISO 8601 timestamp");
const parsed = Date.parse(value);
if (!Number.isFinite(parsed) || parsed <= nowMs) throw new Error("expiresAt must be a future ISO 8601 timestamp");
return parsed;
}
function parseStatus(value) {
if (value === void 0) return;
const statuses = [
"pending",
"armed",
"fired",
"done",
"cancelled",
"expired"
];
if (typeof value !== "string" || !statuses.includes(value)) throw new Error(`status must be one of: ${statuses.join(", ")}`);
return value;
}
function parseScope(value, field, allowed, fallback) {
if (value === void 0) return fallback;
if (typeof value !== "string" || !allowed.includes(value)) throw new Error(`${field} must be one of: ${allowed.join(", ")}`);
return value;
}
function createStandingIntentTool(options) {
return {
label: "Standing Intent",
name: "intent",
description: `Create, list, or explicitly cancel event-conditioned standing intents. Creating an intent arms it immediately. ${STANDING_INTENT_AUTOMATION_GUIDANCE} ${STANDING_INTENT_SCOPE_GUIDANCE} Use scheduled tasks for time-based reminders; use this tool only for events expressed by trigger keywords.`,
parameters: {
type: "object",
properties: {
action: {
type: "string",
enum: [
"create",
"list",
"cancel"
]
},
id: { type: "string" },
description: {
type: "string",
maxLength: INTENT_DESCRIPTION_MAX_CHARS
},
triggerKeywords: {
type: "array",
items: {
type: "string",
maxLength: INTENT_KEYWORD_MAX_CHARS
},
maxItems: INTENT_KEYWORD_MAX_COUNT
},
scope: {
type: "string",
enum: [
"conversation",
"channel",
"anywhere"
],
default: "channel",
description: STANDING_INTENT_SCOPE_GUIDANCE
},
senderScope: {
type: "string",
enum: ["sender", "anyone"],
default: "sender"
},
expiresAt: { type: "string" },
maxFires: {
type: "integer",
minimum: 1
},
cooldownSeconds: {
type: "integer",
minimum: 0
},
status: {
type: "string",
enum: [
"pending",
"armed",
"fired",
"done",
"cancelled",
"expired"
]
}
},
required: ["action"],
additionalProperties: false
},
execute: async (_toolCallId, rawParams) => {
const params = rawParams ?? {};
if (params.action === "create") {
const provider = options.provider?.trim();
const senderId = options.senderId?.trim();
if (!provider || !senderId) throw new Error(`authenticated ${!provider ? senderId ? "channel" : "channel and sender" : "sender"} identity is unavailable for this turn; retry from an authenticated channel conversation`);
const nowMs = Date.now();
const scope = parseScope(params.scope, "scope", [
"conversation",
"channel",
"anywhere"
], "channel");
const senderScope = parseScope(params.senderScope, "senderScope", ["sender", "anyone"], "sender");
const intent = createStandingIntent({
agentId: options.agentId,
description: trimRequiredString(params.description, "description", INTENT_DESCRIPTION_MAX_CHARS),
triggerKeywords: normalizeKeywords(params.triggerKeywords),
channelScope: scope === "anywhere" ? null : encodeStandingIntentChannelScope({
scope,
provider: options.provider ?? "",
accountId: options.accountId,
conversationId: options.conversationId
}),
senderScope: senderScope === "anyone" ? null : encodeStandingIntentSenderScope({
provider: options.provider ?? "",
accountId: options.accountId,
senderId: options.senderId ?? ""
}),
creatorSender: senderId,
expiresAt: parseExpiry(params.expiresAt, nowMs),
maxFires: positiveInteger(params.maxFires, "maxFires", 3),
cooldownSeconds: nonNegativeInteger(params.cooldownSeconds, "cooldownSeconds", DEFAULT_INTENT_COOLDOWN_SECONDS),
sourceSessionId: options.sourceSessionId,
nowMs
});
return jsonResult({
intent,
message: renderArmedIntentMessage(scope)
});
}
if (params.action === "list") return jsonResult({ intents: listStandingIntents({
agentId: options.agentId,
status: parseStatus(params.status)
}) });
if (params.action === "cancel") {
const id = trimRequiredString(params.id, "id", 200);
const intent = cancelStandingIntent({
agentId: options.agentId,
id
});
return jsonResult({
cancelled: intent !== null,
intent
});
}
throw new Error("action must be create, list, or cancel");
}
};
}
//#endregion
export { createStandingIntentTool };