feat: improve tool usage guidance in SOUL.md and add cron.create/cron.delete tools
- SOUL.md: list all available tools (web.search, memory.*, cron.*, etc.)
and add Tool Usage Rules section enforcing 'act, don't narrate'
- cron.ts: add getJob(), addJob(), removeJob() to CronScheduler for
runtime (ephemeral) cron job management
- cron tools: add cron.create and cron.delete tools, enhance cron.list
to show schedule/output/message details
- policy.ts: add cron tools to messaging and coding profiles, add
group:cron to tool groups
Fixes issue where models would narrate tool intent ('let me search...')
then stop without actually calling tools.
This commit is contained in:
+139
-3
@@ -8,7 +8,7 @@ export function createCronTools(scheduler: CronScheduler): Tool[] {
|
||||
const cronList: Tool = {
|
||||
name: 'cron.list',
|
||||
description:
|
||||
'List all configured cron jobs with their names and status.',
|
||||
'List all configured cron jobs with their names, schedules, and messages.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
@@ -21,7 +21,11 @@ export function createCronTools(scheduler: CronScheduler): Tool[] {
|
||||
return { success: true, output: 'No cron jobs configured.' };
|
||||
}
|
||||
|
||||
const lines = jobNames.map((name) => `- ${name}`);
|
||||
const lines = jobNames.map((name) => {
|
||||
const job = scheduler.getJob(name);
|
||||
if (!job) return `- ${name}`;
|
||||
return `- **${name}** — schedule: \`${job.schedule}\`, enabled: ${job.enabled}, output: ${job.output.channel}/${job.output.peer}\n message: "${job.message.length > 80 ? job.message.slice(0, 80) + '...' : job.message}"`;
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
output: `${jobNames.length} cron job(s):\n\n${lines.join('\n')}`,
|
||||
@@ -78,5 +82,137 @@ export function createCronTools(scheduler: CronScheduler): Tool[] {
|
||||
},
|
||||
};
|
||||
|
||||
return [cronList, cronTrigger];
|
||||
const cronCreate: Tool = {
|
||||
name: 'cron.create',
|
||||
description:
|
||||
'Create a new cron job that sends a message on a schedule. The job starts immediately and runs until the daemon restarts (ephemeral). Use standard cron syntax for the schedule (e.g. "0 9 * * *" for daily at 9am, "*/30 * * * *" for every 30 minutes).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Unique name for the cron job (kebab-case, e.g. "apartment-check")',
|
||||
},
|
||||
schedule: {
|
||||
type: 'string',
|
||||
description: 'Cron schedule expression (e.g. "0 9 * * *" for daily at 9am, "0 */6 * * *" for every 6 hours)',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'The message to send when the job fires. This message will be processed as if a user sent it, so you can include instructions for yourself.',
|
||||
},
|
||||
output_channel: {
|
||||
type: 'string',
|
||||
description: 'Channel to send output to (e.g. "telegram", "discord", "slack"). Defaults to "cron" (responses stay in the cron session).',
|
||||
},
|
||||
output_peer: {
|
||||
type: 'string',
|
||||
description: 'Peer ID on the output channel (e.g. Telegram chat ID). Required if output_channel is set.',
|
||||
},
|
||||
timezone: {
|
||||
type: 'string',
|
||||
description: 'IANA timezone (e.g. "America/Los_Angeles"). Defaults to system timezone.',
|
||||
},
|
||||
},
|
||||
required: ['name', 'schedule', 'message'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as {
|
||||
name: string;
|
||||
schedule: string;
|
||||
message: string;
|
||||
output_channel?: string;
|
||||
output_peer?: string;
|
||||
timezone?: string;
|
||||
};
|
||||
|
||||
try {
|
||||
// Validate name format
|
||||
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(args.name)) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'Job name must be kebab-case (lowercase letters, numbers, and hyphens, not starting/ending with hyphen)',
|
||||
};
|
||||
}
|
||||
|
||||
const outputChannel = args.output_channel ?? 'cron';
|
||||
const outputPeer = args.output_peer ?? args.name;
|
||||
|
||||
const created = scheduler.addJob({
|
||||
name: args.name,
|
||||
schedule: args.schedule,
|
||||
message: args.message,
|
||||
output: {
|
||||
channel: outputChannel,
|
||||
peer: outputPeer,
|
||||
},
|
||||
enabled: true,
|
||||
timezone: args.timezone,
|
||||
});
|
||||
|
||||
if (!created) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Cron job "${args.name}" already exists. Use a different name or delete the existing one first.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `Cron job "${args.name}" created and started.\nSchedule: ${args.schedule}${args.timezone ? ` (${args.timezone})` : ''}\nOutput: ${outputChannel}/${outputPeer}\nMessage: "${args.message}"\n\nNote: This job is ephemeral and will not persist across daemon restarts. To make it permanent, add it to the config file.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const cronDelete: Tool = {
|
||||
name: 'cron.delete',
|
||||
description:
|
||||
'Delete a cron job by name. Stops the job immediately. Only works for runtime-created jobs; config-defined jobs will reappear on restart.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the cron job to delete',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as { name: string };
|
||||
|
||||
try {
|
||||
const removed = scheduler.removeJob(args.name);
|
||||
if (!removed) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Cron job "${args.name}" not found. Available: ${scheduler.getJobNames().join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `Cron job "${args.name}" deleted and stopped.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return [cronList, cronTrigger, cronCreate, cronDelete];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user