diff --git a/container/agent-runner/src/ipc-mcp-stdio.ts b/container/agent-runner/src/ipc-mcp-stdio.ts index fc1236d..d95abb6 100644 --- a/container/agent-runner/src/ipc-mcp-stdio.ts +++ b/container/agent-runner/src/ipc-mcp-stdio.ts @@ -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 = { + 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. diff --git a/src/ipc.ts b/src/ipc.ts index d410685..e5614ce 100644 --- a/src/ipc.ts +++ b/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[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) {