feat: add update_task tool and return task ID from schedule_task
schedule_task was creating duplicate tasks when users asked to modify a schedule, because the agent had no way to update an existing task and didn't know the ID of the task it created. Now schedule_task generates and returns the task ID, and a new update_task tool allows modifying prompt, schedule_type, and schedule_value in place. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,7 +64,7 @@ server.tool(
|
||||
|
||||
server.tool(
|
||||
'schedule_task',
|
||||
`Schedule a recurring or one-time task. The task will run as a full agent with access to all tools.
|
||||
`Schedule a recurring or one-time task. The task will run as a full agent with access to all tools. Returns the task ID for future reference. To modify an existing task, use update_task instead.
|
||||
|
||||
CONTEXT MODE - Choose based on task type:
|
||||
\u2022 "group": Task runs in the group's conversation context, with access to chat history. Use for tasks that need context about ongoing discussions, user preferences, or recent interactions.
|
||||
@@ -130,8 +130,11 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
|
||||
// Non-main groups can only schedule for themselves
|
||||
const targetJid = isMain && args.target_group_jid ? args.target_group_jid : chatJid;
|
||||
|
||||
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const data = {
|
||||
type: 'schedule_task',
|
||||
taskId,
|
||||
prompt: args.prompt,
|
||||
schedule_type: args.schedule_type,
|
||||
schedule_value: args.schedule_value,
|
||||
@@ -141,10 +144,10 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const filename = writeIpcFile(TASKS_DIR, data);
|
||||
writeIpcFile(TASKS_DIR, data);
|
||||
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Task scheduled (${filename}): ${args.schedule_type} - ${args.schedule_value}` }],
|
||||
content: [{ type: 'text' as const, text: `Task ${taskId} scheduled: ${args.schedule_type} - ${args.schedule_value}` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -244,6 +247,56 @@ server.tool(
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'update_task',
|
||||
'Update an existing scheduled task. Only provided fields are changed; omitted fields stay the same.',
|
||||
{
|
||||
task_id: z.string().describe('The task ID to update'),
|
||||
prompt: z.string().optional().describe('New prompt for the task'),
|
||||
schedule_type: z.enum(['cron', 'interval', 'once']).optional().describe('New schedule type'),
|
||||
schedule_value: z.string().optional().describe('New schedule value (see schedule_task for format)'),
|
||||
},
|
||||
async (args) => {
|
||||
// Validate schedule_value if provided
|
||||
if (args.schedule_type === 'cron' || (!args.schedule_type && args.schedule_value)) {
|
||||
if (args.schedule_value) {
|
||||
try {
|
||||
CronExpressionParser.parse(args.schedule_value);
|
||||
} catch {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Invalid cron: "${args.schedule_value}".` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (args.schedule_type === 'interval' && args.schedule_value) {
|
||||
const ms = parseInt(args.schedule_value, 10);
|
||||
if (isNaN(ms) || ms <= 0) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Invalid interval: "${args.schedule_value}".` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const data: Record<string, string | undefined> = {
|
||||
type: 'update_task',
|
||||
taskId: args.task_id,
|
||||
groupFolder,
|
||||
isMain: String(isMain),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
if (args.prompt !== undefined) data.prompt = args.prompt;
|
||||
if (args.schedule_type !== undefined) data.schedule_type = args.schedule_type;
|
||||
if (args.schedule_value !== undefined) data.schedule_value = args.schedule_value;
|
||||
|
||||
writeIpcFile(TASKS_DIR, data);
|
||||
|
||||
return { content: [{ type: 'text' as const, text: `Task ${args.task_id} update requested.` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'register_group',
|
||||
`Register a new chat/group so the agent can respond to messages there. Main group only.
|
||||
|
||||
68
src/ipc.ts
68
src/ipc.ts
@@ -247,7 +247,9 @@ export async function processTaskIpc(
|
||||
nextRun = scheduled.toISOString();
|
||||
}
|
||||
|
||||
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const taskId =
|
||||
data.taskId ||
|
||||
`task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const contextMode =
|
||||
data.context_mode === 'group' || data.context_mode === 'isolated'
|
||||
? data.context_mode
|
||||
@@ -325,6 +327,70 @@ export async function processTaskIpc(
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_task':
|
||||
if (data.taskId) {
|
||||
const task = getTaskById(data.taskId);
|
||||
if (!task) {
|
||||
logger.warn(
|
||||
{ taskId: data.taskId, sourceGroup },
|
||||
'Task not found for update',
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (!isMain && task.group_folder !== sourceGroup) {
|
||||
logger.warn(
|
||||
{ taskId: data.taskId, sourceGroup },
|
||||
'Unauthorized task update attempt',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const updates: Parameters<typeof updateTask>[1] = {};
|
||||
if (data.prompt !== undefined) updates.prompt = data.prompt;
|
||||
if (data.schedule_type !== undefined)
|
||||
updates.schedule_type = data.schedule_type as
|
||||
| 'cron'
|
||||
| 'interval'
|
||||
| 'once';
|
||||
if (data.schedule_value !== undefined)
|
||||
updates.schedule_value = data.schedule_value;
|
||||
|
||||
// Recompute next_run if schedule changed
|
||||
if (data.schedule_type || data.schedule_value) {
|
||||
const updatedTask = {
|
||||
...task,
|
||||
...updates,
|
||||
};
|
||||
if (updatedTask.schedule_type === 'cron') {
|
||||
try {
|
||||
const interval = CronExpressionParser.parse(
|
||||
updatedTask.schedule_value,
|
||||
{ tz: TIMEZONE },
|
||||
);
|
||||
updates.next_run = interval.next().toISOString();
|
||||
} catch {
|
||||
logger.warn(
|
||||
{ taskId: data.taskId, value: updatedTask.schedule_value },
|
||||
'Invalid cron in task update',
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else if (updatedTask.schedule_type === 'interval') {
|
||||
const ms = parseInt(updatedTask.schedule_value, 10);
|
||||
if (!isNaN(ms) && ms > 0) {
|
||||
updates.next_run = new Date(Date.now() + ms).toISOString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateTask(data.taskId, updates);
|
||||
logger.info(
|
||||
{ taskId: data.taskId, sourceGroup, updates },
|
||||
'Task updated via IPC',
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'refresh_groups':
|
||||
// Only main group can request a refresh
|
||||
if (isMain) {
|
||||
|
||||
Reference in New Issue
Block a user