fix: improve agent output schema, tool descriptions, and shutdown robustness

- Rename status→outputType, responded/silent→message/log for clarity
- Remove scheduled task special-casing: userMessage now sent for all contexts
- Update schema, tool, and CLAUDE.md descriptions to be clear and
  non-contradictory about communication mechanisms
- Use full tool name mcp__nanoclaw__send_message in docs
- Change schedule_task target_group to accept JID instead of folder name
- Only show target_group_jid parameter to main group agents
- Add defense-in-depth sanitization and error callback to exec() in shutdown
- Use "user or group" consistently (supports both 1:1 and group chats)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-06 20:22:45 +02:00
parent ae177156ec
commit 44f0b3d99c
8 changed files with 68 additions and 63 deletions

View File

@@ -18,7 +18,7 @@ interface ContainerInput {
}
interface AgentResponse {
status: 'responded' | 'silent';
outputType: 'message' | 'log';
userMessage?: string;
internalLog?: string;
}
@@ -26,21 +26,21 @@ interface AgentResponse {
const AGENT_RESPONSE_SCHEMA = {
type: 'object',
properties: {
status: {
outputType: {
type: 'string',
enum: ['responded', 'silent'],
description: 'Use "responded" when you have a message for the user. Use "silent" when the messages don\'t require a response (e.g. the conversation is between other people and doesn\'t involve you, or no trigger/mention was directed at you).',
enum: ['message', 'log'],
description: '"message": the userMessage field contains a message to send to the user or group. "log": the output will not be sent to the user or group.',
},
userMessage: {
type: 'string',
description: 'The message to send to the user. Required when status is "responded".',
description: 'A message to send to the user or group. Include when outputType is "message".',
},
internalLog: {
type: 'string',
description: 'Optional internal note about why you chose this status (for logging, not shown to users).',
description: 'Information that will be logged internally but not sent to the user or group.',
},
},
required: ['status'],
required: ['outputType'],
} as const;
interface ContainerOutput {
@@ -254,7 +254,7 @@ async function main(): Promise<void> {
// Add context for scheduled tasks
let prompt = input.prompt;
if (input.isScheduledTask) {
prompt = `[SCHEDULED TASK - You are running automatically, not in response to a user message. Use mcp__nanoclaw__send_message if needed to communicate with the user.]\n\n${input.prompt}`;
prompt = `[SCHEDULED TASK - The following message was sent automatically and is not coming directly from the user or group.]\n\n${input.prompt}`;
}
try {
@@ -294,13 +294,13 @@ async function main(): Promise<void> {
if (message.type === 'result') {
if (message.subtype === 'success' && message.structured_output) {
result = message.structured_output as AgentResponse;
log(`Agent result: status=${result.status}${result.internalLog ? `, log=${result.internalLog}` : ''}`);
log(`Agent result: outputType=${result.outputType}${result.internalLog ? `, log=${result.internalLog}` : ''}`);
} else if (message.subtype === 'error_max_structured_output_retries') {
// Agent couldn't produce valid structured output — fall back to text result
log('Agent failed to produce structured output, falling back to text');
const textResult = 'result' in message ? (message as { result?: string }).result : null;
if (textResult) {
result = { status: 'responded', userMessage: textResult };
result = { outputType: 'message', userMessage: textResult };
}
}
}
@@ -309,7 +309,7 @@ async function main(): Promise<void> {
log('Agent completed successfully');
writeOutput({
status: 'success',
result: result ?? { status: 'silent' },
result: result ?? { outputType: 'log' },
newSessionId
});

View File

@@ -42,7 +42,7 @@ export function createIpcMcp(ctx: IpcMcpContext) {
tools: [
tool(
'send_message',
'Send a message to the current WhatsApp group. Use this to proactively share information or updates.',
'Send a message to the user or group. The message is delivered immediately while you\'re still running. You can call this multiple times to send multiple messages.',
{
text: z.string().describe('The message text to send')
},
@@ -55,12 +55,12 @@ export function createIpcMcp(ctx: IpcMcpContext) {
timestamp: new Date().toISOString()
};
const filename = writeIpcFile(MESSAGES_DIR, data);
writeIpcFile(MESSAGES_DIR, data);
return {
content: [{
type: 'text',
text: `Message queued for delivery (${filename})`
text: 'Message sent.'
}]
};
}
@@ -71,10 +71,10 @@ export function createIpcMcp(ctx: IpcMcpContext) {
`Schedule a recurring or one-time task. The task will run as a full agent with access to all tools.
CONTEXT MODE - Choose based on task type:
• "group" (recommended for most tasks): Task runs in the group's conversation context, with access to chat history and memory. Use for tasks that need context about ongoing discussions, user preferences, or previous interactions.
• "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.
• "isolated": Task runs in a fresh session with no conversation history. Use for independent tasks that don't need prior context. When using isolated mode, include all necessary context in the prompt itself.
If unsure which mode to use, ask the user. Examples:
If unsure which mode to use, you can ask the user. Examples:
- "Remind me about our discussion" → group (needs conversation context)
- "Check the weather every morning" → isolated (self-contained task)
- "Follow up on my request" → group (needs to know what was requested)
@@ -89,7 +89,7 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
schedule_type: z.enum(['cron', 'interval', 'once']).describe('cron=recurring at specific times, interval=recurring every N ms, once=run once at specific time'),
schedule_value: z.string().describe('cron: "*/5 * * * *" | interval: milliseconds like "300000" | once: local timestamp like "2026-02-01T15:30:00" (no Z suffix!)'),
context_mode: z.enum(['group', 'isolated']).default('group').describe('group=runs with chat history and memory, isolated=fresh session (include context in prompt)'),
target_group: z.string().optional().describe('Target group folder (main only, defaults to current group)')
...(isMain ? { target_group_jid: z.string().optional().describe('JID of the group to schedule the task for. The group must be registered — look up JIDs in /workspace/project/data/registered_groups.json (the keys are JIDs). If the group is not registered, let the user know and ask if they want to activate it. Defaults to the current group.') } : {}),
},
async (args) => {
// Validate schedule_value before writing IPC
@@ -121,7 +121,7 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
}
// Non-main groups can only schedule for themselves
const targetGroup = isMain && args.target_group ? args.target_group : groupFolder;
const targetJid = isMain && args.target_group_jid ? args.target_group_jid : chatJid;
const data = {
type: 'schedule_task',
@@ -129,8 +129,7 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
schedule_type: args.schedule_type,
schedule_value: args.schedule_value,
context_mode: args.context_mode || 'group',
groupFolder: targetGroup,
chatJid,
targetJid,
createdBy: groupFolder,
timestamp: new Date().toISOString()
};