Merge b6467585bf into da71eaebd2
This commit is contained in:
commit
8b77301fce
@ -195,7 +195,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
browser: "Control web browser",
|
||||
canvas: "Present/eval/snapshot the Canvas",
|
||||
nodes: "List/describe/notify/camera/screen on paired nodes",
|
||||
cron: "Manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
|
||||
cron: "Manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate; for simple text reminders that don't need AI processing, use sessionTarget=\"none\" with payload.kind=\"directMessage\" — zero token cost)",
|
||||
message: "Send messages and channel actions",
|
||||
gateway: "Restart, apply config, or run updates on the running OpenClaw process",
|
||||
agents_list: "List agent ids allowed for sessions_spawn",
|
||||
@ -346,7 +346,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
"- browser: control openclaw's dedicated browser",
|
||||
"- canvas: present/eval/snapshot the Canvas",
|
||||
"- nodes: list/describe/notify/camera/screen on paired nodes",
|
||||
"- cron: manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
|
||||
"- cron: manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate; for simple text reminders that don't need AI processing, use sessionTarget=\"none\" with payload.kind=\"directMessage\" — zero token cost)",
|
||||
"- sessions_list: list sessions",
|
||||
"- sessions_history: fetch session history",
|
||||
"- sessions_send: send to another session",
|
||||
|
||||
@ -150,7 +150,7 @@ JOB SCHEMA (for add action):
|
||||
"name": "string (optional)",
|
||||
"schedule": { ... }, // Required: when to run
|
||||
"payload": { ... }, // Required: what to execute
|
||||
"sessionTarget": "main" | "isolated", // Required
|
||||
"sessionTarget": "main" | "isolated" | "none", // Required
|
||||
"enabled": true | false // Optional, default true
|
||||
}
|
||||
|
||||
@ -167,10 +167,13 @@ PAYLOAD TYPES (payload.kind):
|
||||
{ "kind": "systemEvent", "text": "<message>" }
|
||||
- "agentTurn": Runs agent with message (isolated sessions only)
|
||||
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional>, "deliver": <optional-bool>, "channel": "<optional>", "to": "<optional>", "bestEffortDeliver": <optional-bool> }
|
||||
- "directMessage": Sends text directly to a channel WITHOUT AI — zero token cost. Best for simple text reminders.
|
||||
{ "kind": "directMessage", "text": "<message>", "channel": "<optional-channel-id>", "to": "<optional-target>" }
|
||||
|
||||
CRITICAL CONSTRAINTS:
|
||||
- sessionTarget="main" REQUIRES payload.kind="systemEvent"
|
||||
- sessionTarget="isolated" REQUIRES payload.kind="agentTurn"
|
||||
- sessionTarget="none" REQUIRES payload.kind="directMessage"
|
||||
|
||||
WAKE MODES (for wake action):
|
||||
- "next-heartbeat" (default): Wake on next heartbeat
|
||||
|
||||
@ -106,6 +106,7 @@ export function normalizeCronJobInput(
|
||||
const kind = typeof next.payload.kind === "string" ? next.payload.kind : "";
|
||||
if (kind === "systemEvent") next.sessionTarget = "main";
|
||||
if (kind === "agentTurn") next.sessionTarget = "isolated";
|
||||
if (kind === "directMessage") next.sessionTarget = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -131,6 +131,16 @@ function mergeCronPayload(existing: CronPayload, patch: CronPayloadPatch): CronP
|
||||
return { kind: "systemEvent", text };
|
||||
}
|
||||
|
||||
if (patch.kind === "directMessage") {
|
||||
if (existing.kind !== "directMessage") {
|
||||
return buildPayloadFromPatch(patch);
|
||||
}
|
||||
const text = typeof patch.text === "string" ? patch.text : existing.text;
|
||||
const channel = typeof patch.channel === "string" ? patch.channel : existing.channel;
|
||||
const to = typeof patch.to === "string" ? patch.to : existing.to;
|
||||
return { kind: "directMessage", text, channel, to };
|
||||
}
|
||||
|
||||
if (existing.kind !== "agentTurn") {
|
||||
return buildPayloadFromPatch(patch);
|
||||
}
|
||||
@ -157,6 +167,13 @@ function buildPayloadFromPatch(patch: CronPayloadPatch): CronPayload {
|
||||
return { kind: "systemEvent", text: patch.text };
|
||||
}
|
||||
|
||||
if (patch.kind === "directMessage") {
|
||||
if (typeof patch.text !== "string" || patch.text.length === 0) {
|
||||
throw new Error('cron.update payload.kind="directMessage" requires text');
|
||||
}
|
||||
return { kind: "directMessage", text: patch.text, channel: patch.channel, to: patch.to };
|
||||
}
|
||||
|
||||
if (typeof patch.message !== "string" || patch.message.length === 0) {
|
||||
throw new Error('cron.update payload.kind="agentTurn" requires message');
|
||||
}
|
||||
|
||||
@ -55,5 +55,6 @@ export function inferLegacyName(job: {
|
||||
|
||||
export function normalizePayloadToSystemText(payload: CronPayload) {
|
||||
if (payload.kind === "systemEvent") return payload.text.trim();
|
||||
if (payload.kind === "directMessage") return payload.text.trim();
|
||||
return payload.message.trim();
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ export type CronServiceDeps = {
|
||||
outputText?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
sendDirectMessage?: (params: { text: string; channel?: string; to?: string }) => Promise<{ ok: boolean; error?: string }>;
|
||||
onEvent?: (evt: CronEvent) => void;
|
||||
};
|
||||
|
||||
|
||||
@ -137,6 +137,37 @@ export async function executeJob(
|
||||
};
|
||||
|
||||
try {
|
||||
if (job.sessionTarget === "none") {
|
||||
if (job.payload.kind !== "directMessage") {
|
||||
await finish("skipped", 'sessionTarget "none" requires payload.kind="directMessage"');
|
||||
return;
|
||||
}
|
||||
const text = job.payload.text?.trim();
|
||||
if (!text) {
|
||||
await finish("skipped", "directMessage requires non-empty text");
|
||||
return;
|
||||
}
|
||||
if (!state.deps.sendDirectMessage) {
|
||||
await finish("error", "directMessage not supported (sendDirectMessage not wired)");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await state.deps.sendDirectMessage({
|
||||
text,
|
||||
channel: job.payload.channel ?? undefined,
|
||||
to: job.payload.to ?? undefined,
|
||||
});
|
||||
if (res.ok) {
|
||||
await finish("ok", undefined, text);
|
||||
} else {
|
||||
await finish("error", res.error ?? "directMessage send failed");
|
||||
}
|
||||
} catch (sendErr) {
|
||||
await finish("error", String(sendErr));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.sessionTarget === "main") {
|
||||
const text = resolveJobPayloadTextForMain(job);
|
||||
if (!text) {
|
||||
|
||||
@ -5,7 +5,7 @@ export type CronSchedule =
|
||||
| { kind: "every"; everyMs: number; anchorMs?: number }
|
||||
| { kind: "cron"; expr: string; tz?: string };
|
||||
|
||||
export type CronSessionTarget = "main" | "isolated";
|
||||
export type CronSessionTarget = "main" | "isolated" | "none";
|
||||
export type CronWakeMode = "next-heartbeat" | "now";
|
||||
|
||||
export type CronMessageChannel = ChannelId | "last";
|
||||
@ -24,7 +24,8 @@ export type CronPayload =
|
||||
channel?: CronMessageChannel;
|
||||
to?: string;
|
||||
bestEffortDeliver?: boolean;
|
||||
};
|
||||
}
|
||||
| { kind: "directMessage"; text: string; channel?: CronMessageChannel; to?: string };
|
||||
|
||||
export type CronPayloadPatch =
|
||||
| { kind: "systemEvent"; text?: string }
|
||||
@ -39,7 +40,8 @@ export type CronPayloadPatch =
|
||||
channel?: CronMessageChannel;
|
||||
to?: string;
|
||||
bestEffortDeliver?: boolean;
|
||||
};
|
||||
}
|
||||
| { kind: "directMessage"; text?: string; channel?: CronMessageChannel; to?: string };
|
||||
|
||||
export type CronIsolation = {
|
||||
postToMainPrefix?: string;
|
||||
|
||||
@ -50,6 +50,15 @@ export const CronPayloadSchema = Type.Union([
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
Type.Object(
|
||||
{
|
||||
kind: Type.Literal("directMessage"),
|
||||
text: NonEmptyString,
|
||||
channel: Type.Optional(Type.Union([Type.Literal("last"), NonEmptyString])),
|
||||
to: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
]);
|
||||
|
||||
export const CronPayloadPatchSchema = Type.Union([
|
||||
@ -74,6 +83,15 @@ export const CronPayloadPatchSchema = Type.Union([
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
Type.Object(
|
||||
{
|
||||
kind: Type.Literal("directMessage"),
|
||||
text: Type.Optional(NonEmptyString),
|
||||
channel: Type.Optional(Type.Union([Type.Literal("last"), NonEmptyString])),
|
||||
to: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
]);
|
||||
|
||||
export const CronIsolationSchema = Type.Object(
|
||||
@ -110,7 +128,7 @@ export const CronJobSchema = Type.Object(
|
||||
createdAtMs: Type.Integer({ minimum: 0 }),
|
||||
updatedAtMs: Type.Integer({ minimum: 0 }),
|
||||
schedule: CronScheduleSchema,
|
||||
sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated")]),
|
||||
sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated"), Type.Literal("none")]),
|
||||
wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]),
|
||||
payload: CronPayloadSchema,
|
||||
isolation: Type.Optional(CronIsolationSchema),
|
||||
@ -136,7 +154,7 @@ export const CronAddParamsSchema = Type.Object(
|
||||
enabled: Type.Optional(Type.Boolean()),
|
||||
deleteAfterRun: Type.Optional(Type.Boolean()),
|
||||
schedule: CronScheduleSchema,
|
||||
sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated")]),
|
||||
sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated"), Type.Literal("none")]),
|
||||
wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]),
|
||||
payload: CronPayloadSchema,
|
||||
isolation: Type.Optional(CronIsolationSchema),
|
||||
@ -152,7 +170,7 @@ export const CronJobPatchSchema = Type.Object(
|
||||
enabled: Type.Optional(Type.Boolean()),
|
||||
deleteAfterRun: Type.Optional(Type.Boolean()),
|
||||
schedule: Type.Optional(CronScheduleSchema),
|
||||
sessionTarget: Type.Optional(Type.Union([Type.Literal("main"), Type.Literal("isolated")])),
|
||||
sessionTarget: Type.Optional(Type.Union([Type.Literal("main"), Type.Literal("isolated"), Type.Literal("none")])),
|
||||
wakeMode: Type.Optional(Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")])),
|
||||
payload: Type.Optional(CronPayloadPatchSchema),
|
||||
isolation: Type.Optional(CronIsolationSchema),
|
||||
|
||||
@ -8,6 +8,7 @@ import { CronService } from "../cron/service.js";
|
||||
import { resolveCronStorePath } from "../cron/store.js";
|
||||
import { runHeartbeatOnce } from "../infra/heartbeat-runner.js";
|
||||
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
|
||||
import { runMessageAction } from "../infra/outbound/message-action-runner.js";
|
||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||
import { getChildLogger } from "../logging.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
@ -75,6 +76,27 @@ export function buildGatewayCronService(params: {
|
||||
lane: "cron",
|
||||
});
|
||||
},
|
||||
sendDirectMessage: async ({ text, channel, to }) => {
|
||||
const runtimeConfig = loadConfig();
|
||||
try {
|
||||
await runMessageAction({
|
||||
cfg: runtimeConfig,
|
||||
action: "send",
|
||||
params: {
|
||||
message: text,
|
||||
...(channel ? { channel } : {}),
|
||||
...(to ? { target: to } : {}),
|
||||
},
|
||||
gateway: {
|
||||
clientName: "gateway-client",
|
||||
mode: "backend",
|
||||
},
|
||||
});
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, error: String(err) };
|
||||
}
|
||||
},
|
||||
log: getChildLogger({ module: "cron", storePath }),
|
||||
onEvent: (evt) => {
|
||||
params.broadcast("cron", evt, { dropIfSlow: true });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user