From d8261ccf221156aefeed18fa3388959ca7b64ac7 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 21 Jun 2026 17:30:36 +0000 Subject: [PATCH 1/3] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/xlabtg/teleton-agent/issues/679 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 00000000..7a0cf23c --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-06-21T17:30:36.255Z for PR creation at branch issue-679-198121ded3c1 for issue https://github.com/xlabtg/teleton-agent/issues/679 \ No newline at end of file From de1e20e2f57a8bc662cf80249661344be3e4e8c2 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 21 Jun 2026 17:48:15 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=D0=BF=D1=80=D0=B8=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=20pay?= =?UTF-8?q?load=20=D0=B4=D0=BB=D1=8F=20scheduled=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recurring-and-update-tasks.test.ts | 76 ++++++++++++- .../telegram/tasks/create-scheduled-task.ts | 89 ++++----------- src/agent/tools/telegram/tasks/payload.ts | 103 ++++++++++++++++++ src/agent/tools/telegram/tasks/update-task.ts | 68 ++++-------- 4 files changed, 223 insertions(+), 113 deletions(-) create mode 100644 src/agent/tools/telegram/tasks/payload.ts diff --git a/src/agent/tools/telegram/tasks/__tests__/recurring-and-update-tasks.test.ts b/src/agent/tools/telegram/tasks/__tests__/recurring-and-update-tasks.test.ts index e05520a0..304fe12b 100644 --- a/src/agent/tools/telegram/tasks/__tests__/recurring-and-update-tasks.test.ts +++ b/src/agent/tools/telegram/tasks/__tests__/recurring-and-update-tasks.test.ts @@ -1,8 +1,12 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import Database from "better-sqlite3"; +import { validateToolCall, type Tool as PiAiTool, type ToolCall } from "@mariozechner/pi-ai"; import { ensureSchema } from "../../../../../memory/schema.js"; import { getTaskStore } from "../../../../../memory/agent/tasks.js"; -import { telegramCreateScheduledTaskExecutor } from "../create-scheduled-task.js"; +import { + telegramCreateScheduledTaskExecutor, + telegramCreateScheduledTaskTool, +} from "../create-scheduled-task.js"; import { telegramUpdateTaskExecutor } from "../update-task.js"; import { telegramGetTaskExecutor } from "../get-task.js"; import { telegramListTasksExecutor } from "../list-tasks.js"; @@ -131,6 +135,32 @@ describe("telegram_create_scheduled_task with recurrence", () => { db.close(); }); + it("validates object payloads through the tool schema", () => { + const payload = { + type: "tool_call", + tool: "journal_query", + params: { limit: 5 }, + condition: "daily report", + }; + const toolCall: ToolCall = { + type: "toolCall", + id: "call-1", + name: "telegram_create_scheduled_task", + arguments: { + description: "Run journal query", + scheduleDate: futureDate(300), + payload, + }, + }; + + const validated = validateToolCall( + [telegramCreateScheduledTaskTool as unknown as PiAiTool], + toolCall + ) as { payload: unknown }; + + expect(validated.payload).toEqual(payload); + }); + it("creates recurring task with recurrence string", async () => { const ctx = makeContext(db); const result = await telegramCreateScheduledTaskExecutor( @@ -158,6 +188,30 @@ describe("telegram_create_scheduled_task with recurrence", () => { expect(task?.recurrenceInterval).toBe(2700); }); + it("accepts payload objects parsed from JSON tool-call arguments", async () => { + const ctx = makeContext(db); + const payload = { + type: "agent_task", + instructions: "Review the latest TON price and write a journal note", + context: { source: "scheduler" }, + }; + + const result = await telegramCreateScheduledTaskExecutor( + { + description: "Review TON price", + scheduleDate: futureDate(300), + payload, + }, + ctx + ); + + expect(result.success).toBe(true); + + const taskId = (result.data as { taskId: string }).taskId; + const task = getTaskStore(db).getTask(taskId); + expect(task?.payload).toBe(JSON.stringify(payload)); + }); + it("returns undefined recurrenceInterval for non-recurring tasks", async () => { const ctx = makeContext(db); const result = await telegramCreateScheduledTaskExecutor( @@ -289,6 +343,26 @@ describe("telegram_update_task", () => { expect(updated?.payload).toBe(newPayload); }); + it("updates task payload from objects parsed from JSON tool-call arguments", async () => { + const store = getTaskStore(db); + const task = store.createTask({ description: "task" }); + const newPayload = { + type: "tool_call", + tool: "journal_query", + params: { limit: 5 }, + condition: "daily report", + }; + + const result = await telegramUpdateTaskExecutor( + { taskId: task.id, payload: newPayload }, + makeContext(db) + ); + + expect(result.success).toBe(true); + const updated = store.getTask(task.id); + expect(updated?.payload).toBe(JSON.stringify(newPayload)); + }); + it("clears payload when empty string provided", async () => { const store = getTaskStore(db); const task = store.createTask({ diff --git a/src/agent/tools/telegram/tasks/create-scheduled-task.ts b/src/agent/tools/telegram/tasks/create-scheduled-task.ts index 828790df..1b31e3c4 100644 --- a/src/agent/tools/telegram/tasks/create-scheduled-task.ts +++ b/src/agent/tools/telegram/tasks/create-scheduled-task.ts @@ -5,6 +5,12 @@ import { randomLong } from "../../../../utils/gramjs-bigint.js"; import { MAX_DEPENDENTS_PER_TASK } from "../../../../constants/limits.js"; import { getErrorMessage } from "../../../../utils/errors.js"; import { createLogger } from "../../../../utils/logger.js"; +import { + normalizeScheduledTaskPayload, + scheduledTaskPayloadSchema, + type ScheduledTaskPayloadInput, + validateScheduledTaskPayload, +} from "./payload.js"; const log = createLogger("Tools"); @@ -61,7 +67,7 @@ export function parseRecurrenceInterval(recurrence: string): number | null { interface CreateScheduledTaskParams { description: string; scheduleDate?: string; - payload?: string; + payload?: ScheduledTaskPayloadInput; reason?: string; priority?: number; dependsOn?: string[]; @@ -112,22 +118,7 @@ export const telegramCreateScheduledTaskTool: Tool = { "When to execute the task (ISO 8601 format, e.g., '2024-12-25T10:00:00Z' or Unix timestamp). Optional if dependsOn is provided - task will execute when dependencies complete.", }) ), - payload: Type.Optional( - Type.String({ - description: `JSON payload defining what to execute automatically. Two types: - -1. Simple tool call (auto-executed, result fed to you): - {"type":"tool_call","tool":"ton_get_price","params":{},"condition":"price > 5"} - -2. Complex agent task — multi-step instructions the agent executes (e.g., trading automation): - {"type":"agent_task","instructions":"1. Run trade simulation\\n2. Check journal for results\\n3. Send report via telegram_send_message","context":{"chatId":"123"}} - -3. Skip on parent failure (continues even if parent fails): - {"type":"agent_task","instructions":"Send daily report","skipOnParentFailure":false} - -If omitted, task is a simple reminder.`, - }) - ), + payload: Type.Optional(scheduledTaskPayloadSchema), reason: Type.Optional( Type.String({ description: "Why you're scheduling this task (helps with context when executing)", @@ -179,7 +170,7 @@ export const telegramCreateScheduledTaskExecutor: ToolExecutor; + +const scheduledTaskPayloadDescription = `JSON payload defining what to execute automatically. Accepts either a JSON string or an already-parsed JSON object. Two types: +1. Tool call: {"type":"tool_call","tool":"ton_get_price","params":{},"condition":"price > 5"} +2. Agent task: {"type":"agent_task","instructions":"Do something","context":{}} +3. Agent task that continues after parent failure: {"type":"agent_task","instructions":"Send daily report","skipOnParentFailure":false} +If omitted, task is a simple reminder.`; + +function createScheduledTaskPayloadSchema(description: string) { + return Type.Union( + [ + Type.String(), + Type.Object( + {}, + { + additionalProperties: true, + } + ), + ], + { + description, + } + ); +} + +export const scheduledTaskPayloadSchema = createScheduledTaskPayloadSchema( + scheduledTaskPayloadDescription +); + +export const scheduledTaskPayloadUpdateSchema = createScheduledTaskPayloadSchema( + `${scheduledTaskPayloadDescription} +Set to empty string "" to convert to a simple reminder with no automatic execution.` +); + +type NormalizedPayload = + | { success: true; payload: string | undefined } + | { success: false; error: string }; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function normalizeScheduledTaskPayload( + payload: ScheduledTaskPayloadInput | undefined +): NormalizedPayload { + if (payload === undefined) { + return { success: true, payload: undefined }; + } + + if (typeof payload === "string") { + return { success: true, payload }; + } + + if (!isRecord(payload)) { + return { success: false, error: "Payload must be a JSON string or object" }; + } + + try { + return { success: true, payload: JSON.stringify(payload) }; + } catch { + return { success: false, error: "Invalid JSON payload" }; + } +} + +export function validateScheduledTaskPayload(payload: string): string | null { + try { + const parsed = JSON.parse(payload) as unknown; + if (!isRecord(parsed)) { + return "Invalid JSON payload"; + } + + if (!parsed.type || !["tool_call", "agent_task"].includes(String(parsed.type))) { + return 'Payload must have type "tool_call" or "agent_task"'; + } + + if (parsed.type === "tool_call") { + if (!parsed.tool || typeof parsed.tool !== "string") { + return 'tool_call payload requires "tool" field (string)'; + } + if (parsed.params !== undefined && !isRecord(parsed.params)) { + return 'tool_call payload "params" must be an object'; + } + } + + if (parsed.type === "agent_task") { + if (!parsed.instructions || typeof parsed.instructions !== "string") { + return 'agent_task payload requires "instructions" field (string)'; + } + if (parsed.instructions.length < 5) { + return "Instructions too short (min 5 characters)"; + } + if (parsed.context !== undefined && !isRecord(parsed.context)) { + return 'agent_task payload "context" must be an object'; + } + } + + return null; + } catch { + return "Invalid JSON payload"; + } +} diff --git a/src/agent/tools/telegram/tasks/update-task.ts b/src/agent/tools/telegram/tasks/update-task.ts index 09233870..a0f4f49a 100644 --- a/src/agent/tools/telegram/tasks/update-task.ts +++ b/src/agent/tools/telegram/tasks/update-task.ts @@ -4,6 +4,12 @@ import { Api } from "telegram"; import { randomLong } from "../../../../utils/gramjs-bigint.js"; import { getErrorMessage } from "../../../../utils/errors.js"; import { createLogger } from "../../../../utils/logger.js"; +import { + normalizeScheduledTaskPayload, + scheduledTaskPayloadUpdateSchema, + type ScheduledTaskPayloadInput, + validateScheduledTaskPayload, +} from "./payload.js"; const log = createLogger("Tools"); @@ -13,7 +19,7 @@ const log = createLogger("Tools"); interface UpdateTaskParams { taskId: string; description?: string; - payload?: string; + payload?: ScheduledTaskPayloadInput; reason?: string; priority?: number; rescheduleDate?: string; @@ -37,14 +43,7 @@ export const telegramUpdateTaskTool: Tool = { description: "New task description", }) ), - payload: Type.Optional( - Type.String({ - description: `New JSON payload for task execution. Same format as telegram_create_scheduled_task: -1. Tool call: {"type":"tool_call","tool":"ton_get_price","params":{},"condition":"price > 5"} -2. Agent task: {"type":"agent_task","instructions":"Do something","context":{}} -Set to empty string "" to convert to a simple reminder with no automatic execution.`, - }) - ), + payload: Type.Optional(scheduledTaskPayloadUpdateSchema), reason: Type.Optional( Type.String({ description: "New reason for the task", @@ -128,42 +127,21 @@ export const telegramUpdateTaskExecutor: ToolExecutor = async }; } - // Validate payload if provided - if (params.payload !== undefined && params.payload !== "") { - try { - const parsed = JSON.parse(params.payload); - if (!parsed.type || !["tool_call", "agent_task"].includes(parsed.type)) { - return { - success: false, - error: 'Payload must have type "tool_call" or "agent_task"', - }; - } - if (parsed.type === "tool_call") { - if (!parsed.tool || typeof parsed.tool !== "string") { - return { - success: false, - error: 'tool_call payload requires "tool" field (string)', - }; - } - } - if (parsed.type === "agent_task") { - if (!parsed.instructions || typeof parsed.instructions !== "string") { - return { - success: false, - error: 'agent_task payload requires "instructions" field (string)', - }; - } - if (parsed.instructions.length < 5) { - return { - success: false, - error: "Instructions too short (min 5 characters)", - }; - } - } - } catch { + const normalizedPayload = normalizeScheduledTaskPayload(params.payload); + if (!normalizedPayload.success) { + return { + success: false, + error: normalizedPayload.error, + }; + } + + // Validate payload if provided. Empty string still means "clear payload". + if (normalizedPayload.payload !== undefined && normalizedPayload.payload !== "") { + const error = validateScheduledTaskPayload(normalizedPayload.payload); + if (error) { return { success: false, - error: "Invalid JSON payload", + error, }; } } @@ -243,7 +221,9 @@ export const telegramUpdateTaskExecutor: ToolExecutor = async if (params.payload !== undefined) { extraFields.push("payload = ?"); - extraValues.push(params.payload === "" ? null : params.payload); + extraValues.push( + normalizedPayload.payload === "" ? null : (normalizedPayload.payload ?? null) + ); } if (params.reason !== undefined) { extraFields.push("reason = ?"); From 8bd4238c5b3f6ea2b2b5c33869924a382b453fb2 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 21 Jun 2026 17:56:30 +0000 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=D1=83=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20.gitkeep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitkeep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 7a0cf23c..00000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-06-21T17:30:36.255Z for PR creation at branch issue-679-198121ded3c1 for issue https://github.com/xlabtg/teleton-agent/issues/679 \ No newline at end of file