arisa 3.0.12 → 3.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/core/agent/agent-manager.js +64 -3
- package/src/core/tasks/task-store.js +165 -0
- package/src/core/tools/tool-registry.js +5 -1
- package/src/runtime/create-app.js +4 -2
- package/src/runtime/paths.js +1 -0
- package/src/transport/telegram/bot.js +118 -50
- package/tools/schedule-agent-task/config.js +1 -0
- package/tools/schedule-agent-task/index.js +68 -0
- package/tools/schedule-agent-task/package.json +6 -0
- package/tools/schedule-agent-task/tool.manifest.json +8 -0
- package/tools/web-browser/index.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.14",
|
|
4
4
|
"description": "Telegram + Pi Agent modular assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"pi.dev",
|
|
31
31
|
"clasen"
|
|
32
32
|
],
|
|
33
|
-
"author": "",
|
|
34
|
-
"license": "
|
|
33
|
+
"author": "Martin Clasen",
|
|
34
|
+
"license": "MIT",
|
|
35
35
|
"packageManager": "pnpm@10.32.1",
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@mariozechner/pi-coding-agent": "^0.65.0",
|
|
@@ -8,10 +8,11 @@ import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js"
|
|
|
8
8
|
import { arisaHomeDir } from "../../runtime/paths.js";
|
|
9
9
|
|
|
10
10
|
export class AgentManager {
|
|
11
|
-
constructor({ config, artifactStore, toolRegistry, logger }) {
|
|
11
|
+
constructor({ config, artifactStore, toolRegistry, taskStore, logger }) {
|
|
12
12
|
this.config = config;
|
|
13
13
|
this.artifactStore = artifactStore;
|
|
14
14
|
this.toolRegistry = toolRegistry;
|
|
15
|
+
this.taskStore = taskStore;
|
|
15
16
|
this.logger = logger;
|
|
16
17
|
this.sessions = new Map();
|
|
17
18
|
}
|
|
@@ -65,7 +66,7 @@ export class AgentManager {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
this.logger?.log("agent", `creating session for chat ${chatId}`);
|
|
68
|
-
const customTools = this.createTools(telegram);
|
|
69
|
+
const customTools = this.createTools(telegram, chatId);
|
|
69
70
|
const { session } = await createAgentSession({
|
|
70
71
|
cwd: arisaInstallDir,
|
|
71
72
|
agentDir: arisaHomeDir,
|
|
@@ -87,7 +88,7 @@ export class AgentManager {
|
|
|
87
88
|
return ctx;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
createTools(telegram) {
|
|
91
|
+
createTools(telegram, chatId) {
|
|
91
92
|
return [
|
|
92
93
|
defineTool({
|
|
93
94
|
name: "list_tools",
|
|
@@ -175,12 +176,72 @@ export class AgentManager {
|
|
|
175
176
|
await unlink(result.output.filePath).catch(() => {});
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
if (result.asyncTask || result.asyncTasks?.length) {
|
|
180
|
+
const scheduled = await this.taskStore.addMany(
|
|
181
|
+
result.asyncTasks || [result.asyncTask],
|
|
182
|
+
{
|
|
183
|
+
payload: { chatId },
|
|
184
|
+
source: { type: "tool", toolName: params.name, chatId }
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
result.asyncTasks = scheduled;
|
|
188
|
+
delete result.asyncTask;
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
return {
|
|
179
192
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
180
193
|
details: result
|
|
181
194
|
};
|
|
182
195
|
}
|
|
183
196
|
}),
|
|
197
|
+
defineTool({
|
|
198
|
+
name: "list_scheduled_tasks",
|
|
199
|
+
label: "List scheduled tasks",
|
|
200
|
+
description: "List scheduled async tasks for the current Telegram chat.",
|
|
201
|
+
parameters: Type.Object({
|
|
202
|
+
status: Type.Optional(Type.String())
|
|
203
|
+
}),
|
|
204
|
+
execute: async (_id, params) => {
|
|
205
|
+
const tasks = await this.taskStore.list({ chatId, status: params.status });
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }],
|
|
208
|
+
details: { tasks }
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}),
|
|
212
|
+
defineTool({
|
|
213
|
+
name: "cancel_scheduled_task",
|
|
214
|
+
label: "Cancel scheduled task",
|
|
215
|
+
description: "Cancel one scheduled async task by id for the current Telegram chat.",
|
|
216
|
+
parameters: Type.Object({ id: Type.String() }),
|
|
217
|
+
execute: async (_id, params) => {
|
|
218
|
+
const existing = await this.taskStore.get(params.id);
|
|
219
|
+
if (!existing || existing.payload?.chatId !== chatId) {
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Task not found" }) }],
|
|
222
|
+
details: { ok: false, error: "Task not found" }
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const task = await this.taskStore.cancel(params.id);
|
|
226
|
+
return {
|
|
227
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, task }, null, 2) }],
|
|
228
|
+
details: { ok: true, task }
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}),
|
|
232
|
+
defineTool({
|
|
233
|
+
name: "cancel_all_scheduled_tasks",
|
|
234
|
+
label: "Cancel all scheduled tasks",
|
|
235
|
+
description: "Cancel all pending or running async tasks for the current Telegram chat.",
|
|
236
|
+
parameters: Type.Object({}),
|
|
237
|
+
execute: async () => {
|
|
238
|
+
const tasks = await this.taskStore.cancelAll({ chatId });
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, cancelled: tasks.length }, null, 2) }],
|
|
241
|
+
details: { ok: true, tasks }
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}),
|
|
184
245
|
defineTool({
|
|
185
246
|
name: "send_media_reply",
|
|
186
247
|
label: "Send media reply",
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { tasksFile } from "../../runtime/paths.js";
|
|
5
|
+
|
|
6
|
+
async function loadTasksFile() {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(await readFile(tasksFile, "utf8"));
|
|
9
|
+
} catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function saveTasksFile(tasks) {
|
|
15
|
+
await mkdir(path.dirname(tasksFile), { recursive: true });
|
|
16
|
+
await writeFile(tasksFile, `${JSON.stringify(tasks, null, 2)}\n`, "utf8");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function taskId() {
|
|
20
|
+
return crypto.randomUUID();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeTask(task, defaults = {}) {
|
|
24
|
+
return {
|
|
25
|
+
id: task.id || taskId(),
|
|
26
|
+
status: task.status || "pending",
|
|
27
|
+
createdAt: task.createdAt || new Date().toISOString(),
|
|
28
|
+
updatedAt: new Date().toISOString(),
|
|
29
|
+
kind: task.kind,
|
|
30
|
+
runAt: task.runAt,
|
|
31
|
+
payload: {
|
|
32
|
+
...(defaults.payload || {}),
|
|
33
|
+
...(task.payload || {})
|
|
34
|
+
},
|
|
35
|
+
recurrence: task.recurrence || defaults.recurrence || null,
|
|
36
|
+
source: {
|
|
37
|
+
...(defaults.source || {}),
|
|
38
|
+
...(task.source || {})
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function computeNextRunAt(task) {
|
|
44
|
+
if (task.recurrence?.type === "interval" && Number(task.recurrence.everySeconds) > 0) {
|
|
45
|
+
return new Date(Date.now() + (Number(task.recurrence.everySeconds) * 1000)).toISOString();
|
|
46
|
+
}
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class TaskStore {
|
|
51
|
+
constructor() {
|
|
52
|
+
this.tasks = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async init() {
|
|
56
|
+
if (!this.tasks) this.tasks = await loadTasksFile();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async save() {
|
|
60
|
+
await saveTasksFile(this.tasks || []);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async add(task, defaults = {}) {
|
|
64
|
+
await this.init();
|
|
65
|
+
const normalized = normalizeTask(task, defaults);
|
|
66
|
+
this.tasks.push(normalized);
|
|
67
|
+
await this.save();
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async addMany(tasks = [], defaults = {}) {
|
|
72
|
+
const created = [];
|
|
73
|
+
for (const task of tasks) {
|
|
74
|
+
created.push(await this.add(task, defaults));
|
|
75
|
+
}
|
|
76
|
+
return created;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async claimDue(limit = 10) {
|
|
80
|
+
await this.init();
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const due = [];
|
|
83
|
+
|
|
84
|
+
for (const task of this.tasks) {
|
|
85
|
+
if (due.length >= limit) break;
|
|
86
|
+
if (task.status !== "pending") continue;
|
|
87
|
+
if (!task.runAt || Number.isNaN(Date.parse(task.runAt))) continue;
|
|
88
|
+
if (Date.parse(task.runAt) > now) continue;
|
|
89
|
+
task.status = "running";
|
|
90
|
+
task.updatedAt = new Date().toISOString();
|
|
91
|
+
due.push({ ...task });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (due.length) await this.save();
|
|
95
|
+
return due;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async complete(taskId) {
|
|
99
|
+
await this.init();
|
|
100
|
+
const task = this.tasks.find((item) => item.id === taskId);
|
|
101
|
+
if (!task) return null;
|
|
102
|
+
|
|
103
|
+
const nextRunAt = computeNextRunAt(task);
|
|
104
|
+
if (nextRunAt) {
|
|
105
|
+
task.status = "pending";
|
|
106
|
+
task.runAt = nextRunAt;
|
|
107
|
+
task.lastRunAt = new Date().toISOString();
|
|
108
|
+
} else {
|
|
109
|
+
task.status = "done";
|
|
110
|
+
task.completedAt = new Date().toISOString();
|
|
111
|
+
}
|
|
112
|
+
task.updatedAt = new Date().toISOString();
|
|
113
|
+
await this.save();
|
|
114
|
+
return task;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async fail(taskId, error) {
|
|
118
|
+
await this.init();
|
|
119
|
+
const task = this.tasks.find((item) => item.id === taskId);
|
|
120
|
+
if (!task) return null;
|
|
121
|
+
task.status = "failed";
|
|
122
|
+
task.error = error;
|
|
123
|
+
task.updatedAt = new Date().toISOString();
|
|
124
|
+
await this.save();
|
|
125
|
+
return task;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async list(filter = {}) {
|
|
129
|
+
await this.init();
|
|
130
|
+
return this.tasks.filter((task) => {
|
|
131
|
+
if (filter.chatId && task.payload?.chatId !== filter.chatId) return false;
|
|
132
|
+
if (filter.status && task.status !== filter.status) return false;
|
|
133
|
+
if (filter.kind && task.kind !== filter.kind) return false;
|
|
134
|
+
return true;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async get(taskId) {
|
|
139
|
+
await this.init();
|
|
140
|
+
return this.tasks.find((item) => item.id === taskId) || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async cancel(taskId) {
|
|
144
|
+
await this.init();
|
|
145
|
+
const index = this.tasks.findIndex((item) => item.id === taskId);
|
|
146
|
+
if (index === -1) return null;
|
|
147
|
+
const [task] = this.tasks.splice(index, 1);
|
|
148
|
+
await this.save();
|
|
149
|
+
return task;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async cancelAll(filter = {}) {
|
|
153
|
+
await this.init();
|
|
154
|
+
const removed = [];
|
|
155
|
+
this.tasks = this.tasks.filter((task) => {
|
|
156
|
+
if (filter.chatId && task.payload?.chatId !== filter.chatId) return true;
|
|
157
|
+
if (filter.status && task.status !== filter.status) return true;
|
|
158
|
+
if (task.status === "done" || task.status === "failed") return true;
|
|
159
|
+
removed.push({ ...task });
|
|
160
|
+
return false;
|
|
161
|
+
});
|
|
162
|
+
if (removed.length) await this.save();
|
|
163
|
+
return removed;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -119,7 +119,11 @@ export class ToolRegistry {
|
|
|
119
119
|
try {
|
|
120
120
|
const parsed = JSON.parse(result.stdout || result.stderr);
|
|
121
121
|
const normalized = normalizeToolResult(name, parsed);
|
|
122
|
-
|
|
122
|
+
if (normalized.ok === false) {
|
|
123
|
+
this.logger?.log("tools", `${name} -> ${normalized.status || "error"}: ${normalized.error || "unknown error"}`);
|
|
124
|
+
} else {
|
|
125
|
+
this.logger?.log("tools", `${name} -> ok`);
|
|
126
|
+
}
|
|
123
127
|
return normalized;
|
|
124
128
|
} catch {
|
|
125
129
|
return normalizeToolResult(name, {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadConfig, saveConfig, updateConfig } from "../core/config/config-store.js";
|
|
2
2
|
import { ArtifactStore } from "../core/artifacts/artifact-store.js";
|
|
3
3
|
import { ToolRegistry } from "../core/tools/tool-registry.js";
|
|
4
|
+
import { TaskStore } from "../core/tasks/task-store.js";
|
|
4
5
|
import { AgentManager } from "../core/agent/agent-manager.js";
|
|
5
6
|
import { createTelegramBot } from "../transport/telegram/bot.js";
|
|
6
7
|
|
|
@@ -9,11 +10,12 @@ export async function createApp({ logger } = {}) {
|
|
|
9
10
|
const config = await loadConfig();
|
|
10
11
|
const artifactStore = new ArtifactStore();
|
|
11
12
|
const toolRegistry = new ToolRegistry({ logger });
|
|
13
|
+
const taskStore = new TaskStore();
|
|
12
14
|
await toolRegistry.load();
|
|
13
15
|
logger?.log("app", `loaded ${toolRegistry.list().length} tools`);
|
|
14
16
|
|
|
15
|
-
const agentManager = new AgentManager({ config, artifactStore, toolRegistry, logger });
|
|
16
|
-
const bot = await createTelegramBot({ config, artifactStore, toolRegistry, agentManager, saveConfig, updateConfig, logger });
|
|
17
|
+
const agentManager = new AgentManager({ config, artifactStore, toolRegistry, taskStore, logger });
|
|
18
|
+
const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger });
|
|
17
19
|
|
|
18
20
|
return {
|
|
19
21
|
async start() {
|
package/src/runtime/paths.js
CHANGED
|
@@ -9,6 +9,7 @@ export const servicePidFile = path.join(stateDir, "arisa.pid");
|
|
|
9
9
|
export const serviceLogFile = path.join(stateDir, "arisa.log");
|
|
10
10
|
export const artifactsDir = path.join(arisaHomeDir, "artifacts");
|
|
11
11
|
export const artifactsIndexFile = path.join(stateDir, "artifacts.json");
|
|
12
|
+
export const tasksFile = path.join(stateDir, "tasks.json");
|
|
12
13
|
export const toolsDir = path.join(arisaHomeDir, "tools");
|
|
13
14
|
|
|
14
15
|
export function getToolDir(toolName) {
|
|
@@ -32,14 +32,20 @@ function quotedMessageSummary(message) {
|
|
|
32
32
|
return parts;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function getTelegramCommand(ctx) {
|
|
36
|
+
const text = ctx.message?.text || "";
|
|
37
|
+
const entity = ctx.message?.entities?.[0];
|
|
38
|
+
if (entity?.type !== "bot_command" || entity.offset !== 0 || !text.startsWith("/")) return "";
|
|
39
|
+
return text.slice(1, entity.length).split("@")[0].trim().toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
function buildPrompt({ ctx, artifact, transcript, toolResult }) {
|
|
36
43
|
const parts = [
|
|
37
|
-
`New
|
|
44
|
+
`New Session..`,
|
|
38
45
|
`chatId: ${ctx.chat.id}`,
|
|
39
46
|
`userId: ${ctx.from.id}`,
|
|
40
47
|
`username: ${ctx.from.username || "(no username)"}`,
|
|
41
|
-
`messageId: ${ctx.msg.message_id}
|
|
42
|
-
`preferredTelegramLanguageCode: ${ctx.from?.language_code || "unknown"}`
|
|
48
|
+
`messageId: ${ctx.msg.message_id}`
|
|
43
49
|
];
|
|
44
50
|
|
|
45
51
|
if (ctx.message?.text) parts.push(`text: ${ctx.message.text}`);
|
|
@@ -64,6 +70,26 @@ function buildPrompt({ ctx, artifact, transcript, toolResult }) {
|
|
|
64
70
|
return parts.join("\n");
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
function buildNewSessionPrompt(ctx) {
|
|
74
|
+
return [
|
|
75
|
+
"System event: /new requested.",
|
|
76
|
+
"Session was reset.",
|
|
77
|
+
`preferredTelegramLanguageCode: ${ctx.from?.language_code || "unknown"}`,
|
|
78
|
+
"Reply with a brief, warm confirmation in the user's language."
|
|
79
|
+
].join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildAsyncTaskPrompt(task) {
|
|
83
|
+
return [
|
|
84
|
+
"Scheduled task fired.",
|
|
85
|
+
`taskId: ${task.id}`,
|
|
86
|
+
`chatId: ${task.payload.chatId}`,
|
|
87
|
+
task.payload.prompt ? `text: ${task.payload.prompt}` : null,
|
|
88
|
+
"Treat this as a new request for the chat and fulfill it now.",
|
|
89
|
+
"If needed, use tools."
|
|
90
|
+
].filter(Boolean).join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
async function maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactStore }) {
|
|
68
94
|
if (!artifact || artifact.kind !== "audio") return { transcript: null };
|
|
69
95
|
|
|
@@ -117,7 +143,7 @@ async function withTyping(ctx, work) {
|
|
|
117
143
|
}
|
|
118
144
|
}
|
|
119
145
|
|
|
120
|
-
export async function createTelegramBot({ config, artifactStore, toolRegistry, agentManager, saveConfig, updateConfig, logger }) {
|
|
146
|
+
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger }) {
|
|
121
147
|
const bot = new Bot(config.telegram.apiKey);
|
|
122
148
|
const perChatState = new Map();
|
|
123
149
|
|
|
@@ -172,51 +198,58 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
172
198
|
await sendText(renderTelegramHtml(text), { parse_mode: "HTML" });
|
|
173
199
|
}
|
|
174
200
|
|
|
175
|
-
|
|
176
|
-
|
|
201
|
+
function createTelegramSessionBridge(chatId) {
|
|
202
|
+
return {
|
|
177
203
|
sendMedia: async (filePath, { method = "audio", caption } = {}) => {
|
|
178
|
-
logger?.log("telegram", `sending ${method} reply for chat ${
|
|
204
|
+
logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
|
|
179
205
|
const input = new InputFile(filePath);
|
|
180
|
-
if (method === "voice") return
|
|
181
|
-
if (method === "document") return
|
|
182
|
-
return
|
|
206
|
+
if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
|
|
207
|
+
if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
|
|
208
|
+
return bot.api.sendAudio(chatId, input, { caption });
|
|
183
209
|
}
|
|
184
210
|
};
|
|
185
|
-
|
|
186
|
-
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function processPromptForChat({ chatId, prompt, ctx = null }) {
|
|
214
|
+
const work = async () => {
|
|
215
|
+
const { session } = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
|
|
187
216
|
const text = await collectText(session, prompt);
|
|
188
217
|
if (text) {
|
|
189
218
|
await sendTextReply({
|
|
190
|
-
sendText: (message, extra) =>
|
|
191
|
-
sendDocument: (file, extra) =>
|
|
192
|
-
chatId
|
|
219
|
+
sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
|
|
220
|
+
sendDocument: (file, extra) => bot.api.sendDocument(chatId, file, extra),
|
|
221
|
+
chatId,
|
|
193
222
|
text
|
|
194
223
|
});
|
|
195
224
|
}
|
|
196
|
-
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (ctx) return withTyping(ctx, work);
|
|
228
|
+
return work();
|
|
197
229
|
}
|
|
198
230
|
|
|
199
|
-
async function
|
|
200
|
-
const chatState = getChatState(
|
|
201
|
-
const incomingPrompt = await buildIncomingPrompt(ctx);
|
|
231
|
+
async function enqueuePrompt({ chatId, prompt, label, ctx = null }) {
|
|
232
|
+
const chatState = getChatState(chatId);
|
|
202
233
|
|
|
203
234
|
if (chatState.processing) {
|
|
204
|
-
logger?.log("telegram", `chat ${
|
|
235
|
+
logger?.log("telegram", `chat ${chatId} busy, queueing ${label}`);
|
|
205
236
|
chatState.nextPrompt = chatState.nextPrompt
|
|
206
|
-
? `${chatState.nextPrompt}\n\n${
|
|
207
|
-
:
|
|
208
|
-
return
|
|
237
|
+
? `${chatState.nextPrompt}\n\n${prompt}`
|
|
238
|
+
: prompt;
|
|
239
|
+
return;
|
|
209
240
|
}
|
|
210
241
|
|
|
211
242
|
chatState.processing = true;
|
|
212
|
-
logger?.log("telegram", `processing
|
|
213
|
-
let currentPrompt =
|
|
243
|
+
logger?.log("telegram", `processing ${label} in chat ${chatId}`);
|
|
244
|
+
let currentPrompt = prompt;
|
|
245
|
+
let currentCtx = ctx;
|
|
214
246
|
|
|
215
247
|
while (currentPrompt) {
|
|
216
248
|
try {
|
|
217
|
-
logger?.log("telegram", `prompt dispatch for chat ${
|
|
218
|
-
await
|
|
249
|
+
logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
|
|
250
|
+
await processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
|
|
219
251
|
} finally {
|
|
252
|
+
currentCtx = null;
|
|
220
253
|
if (chatState.nextPrompt) {
|
|
221
254
|
currentPrompt = chatState.nextPrompt;
|
|
222
255
|
chatState.nextPrompt = "";
|
|
@@ -229,6 +262,38 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
229
262
|
chatState.processing = false;
|
|
230
263
|
}
|
|
231
264
|
|
|
265
|
+
async function enqueueOrProcess(ctx) {
|
|
266
|
+
const chatState = getChatState(ctx.chat.id);
|
|
267
|
+
|
|
268
|
+
if (chatState.processing) {
|
|
269
|
+
const incomingPrompt = await buildIncomingPrompt(ctx);
|
|
270
|
+
return enqueuePrompt({
|
|
271
|
+
chatId: ctx.chat.id,
|
|
272
|
+
prompt: incomingPrompt,
|
|
273
|
+
label: `message ${ctx.msg.message_id}`
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const incomingPrompt = await buildIncomingPrompt(ctx);
|
|
278
|
+
return enqueuePrompt({
|
|
279
|
+
chatId: ctx.chat.id,
|
|
280
|
+
prompt: incomingPrompt,
|
|
281
|
+
label: `message ${ctx.msg.message_id}`,
|
|
282
|
+
ctx
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function handleNewCommand(ctx) {
|
|
287
|
+
agentManager.resetSession(ctx.chat.id);
|
|
288
|
+
perChatState.set(ctx.chat.id, { processing: false, nextPrompt: "" });
|
|
289
|
+
await enqueuePrompt({
|
|
290
|
+
chatId: ctx.chat.id,
|
|
291
|
+
prompt: buildNewSessionPrompt(ctx),
|
|
292
|
+
label: "new-session command",
|
|
293
|
+
ctx
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
232
297
|
bot.catch((error) => {
|
|
233
298
|
logger?.error("telegram", `bot error: ${error instanceof Error ? error.message : String(error)}`);
|
|
234
299
|
console.error("Telegram bot error:", error);
|
|
@@ -243,15 +308,16 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
243
308
|
bot.command("new", async (ctx) => {
|
|
244
309
|
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
245
310
|
if (!auth.ok) return;
|
|
246
|
-
|
|
247
|
-
perChatState.set(ctx.chat.id, { processing: false, nextPrompt: "" });
|
|
248
|
-
return ctx.reply("Started a new chat context.");
|
|
311
|
+
await handleNewCommand(ctx);
|
|
249
312
|
});
|
|
250
313
|
|
|
251
314
|
bot.on("message", async (ctx) => {
|
|
252
315
|
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
253
316
|
if (!auth.ok) return;
|
|
254
317
|
|
|
318
|
+
const command = getTelegramCommand(ctx);
|
|
319
|
+
if (command) return;
|
|
320
|
+
|
|
255
321
|
try {
|
|
256
322
|
await enqueueOrProcess(ctx);
|
|
257
323
|
} catch (error) {
|
|
@@ -269,16 +335,6 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
269
335
|
try {
|
|
270
336
|
logger?.log("telegram", `generating startup message for chat ${chatId}`);
|
|
271
337
|
const chatMeta = config.telegram.chatMeta[chatId] || {};
|
|
272
|
-
const telegram = {
|
|
273
|
-
sendMedia: async (filePath, { method = "audio", caption } = {}) => {
|
|
274
|
-
logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
|
|
275
|
-
const input = new InputFile(filePath);
|
|
276
|
-
if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
|
|
277
|
-
if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
|
|
278
|
-
return bot.api.sendAudio(chatId, input, { caption });
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
const { session } = await agentManager.getSessionContext(chatId, telegram);
|
|
282
338
|
const welcomePrompt = [
|
|
283
339
|
"System event: Arisa has just started.",
|
|
284
340
|
`chatId: ${chatId}`,
|
|
@@ -290,15 +346,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
290
346
|
"Use the user's Telegram language when possible.",
|
|
291
347
|
"Do not mention internal implementation details."
|
|
292
348
|
].filter(Boolean).join("\n");
|
|
293
|
-
|
|
294
|
-
if (text) {
|
|
295
|
-
await sendTextReply({
|
|
296
|
-
sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
|
|
297
|
-
sendDocument: (file, extra) => bot.api.sendDocument(chatId, file, extra),
|
|
298
|
-
chatId,
|
|
299
|
-
text
|
|
300
|
-
});
|
|
301
|
-
}
|
|
349
|
+
await enqueuePrompt({ chatId, prompt: welcomePrompt, label: "startup message" });
|
|
302
350
|
} catch (error) {
|
|
303
351
|
logger?.log("telegram", `startup message failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
304
352
|
}
|
|
@@ -306,8 +354,28 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
306
354
|
await bot.api.setMyCommands([
|
|
307
355
|
{ command: "new", description: "Start a new chat context" }
|
|
308
356
|
]);
|
|
357
|
+
setInterval(async () => {
|
|
358
|
+
const tasks = await taskStore.claimDue(10);
|
|
359
|
+
for (const task of tasks) {
|
|
360
|
+
try {
|
|
361
|
+
if (task.kind !== "agent_task" || !task.payload?.chatId || !task.payload?.prompt) {
|
|
362
|
+
await taskStore.fail(task.id, `Unsupported task: ${task.kind}`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
logger?.log("tasks", `running task ${task.id} for chat ${task.payload.chatId}`);
|
|
366
|
+
await enqueuePrompt({
|
|
367
|
+
chatId: task.payload.chatId,
|
|
368
|
+
prompt: buildAsyncTaskPrompt(task),
|
|
369
|
+
label: `scheduled task ${task.id}`
|
|
370
|
+
});
|
|
371
|
+
await taskStore.complete(task.id);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
await taskStore.fail(task.id, error instanceof Error ? error.message : String(error));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}, 1000).unref();
|
|
309
377
|
logger?.log("telegram", "bot polling started");
|
|
310
|
-
await bot.start();
|
|
378
|
+
await bot.start({ drop_pending_updates: true });
|
|
311
379
|
}
|
|
312
380
|
};
|
|
313
381
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { toolError, toolOk } from "../../src/core/tools/tool-result.js";
|
|
3
|
+
|
|
4
|
+
function printHelp() {
|
|
5
|
+
console.log(`schedule-agent-task\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "text": "tell me the temperature in Toronto",\n "artifact": { "text": "tell me the temperature in Toronto" },\n "args": {\n "prompt": "tell me the temperature in Toronto",\n "runAt": "2026-04-07T14:00:00.000Z",\n "delaySeconds": "30",\n "intervalSeconds": "3600"\n }\n }\n\nBehavior:\n - schedules a future agent task for the current chat\n - provide either args.runAt or args.delaySeconds\n - optional args.intervalSeconds makes the task recurring\n`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function firstNonEmpty(...values) {
|
|
9
|
+
return values.find((value) => String(value || "").trim()) || "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildRunAt(args = {}) {
|
|
13
|
+
const runAtValue = firstNonEmpty(args.runAt, args.at, args.when);
|
|
14
|
+
if (runAtValue) {
|
|
15
|
+
const parsed = Date.parse(runAtValue);
|
|
16
|
+
if (Number.isNaN(parsed)) return "";
|
|
17
|
+
return new Date(parsed).toISOString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const delaySeconds = Number(firstNonEmpty(args.delaySeconds, args.delay, args.seconds));
|
|
21
|
+
if (Number.isFinite(delaySeconds) && delaySeconds > 0) {
|
|
22
|
+
return new Date(Date.now() + (delaySeconds * 1000)).toISOString();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function run(requestFile) {
|
|
29
|
+
const request = JSON.parse(await readFile(requestFile, "utf8"));
|
|
30
|
+
const args = request.args || {};
|
|
31
|
+
const prompt = firstNonEmpty(args.prompt, args.message, args.task, request.text, request.artifact?.text);
|
|
32
|
+
const runAt = buildRunAt(args);
|
|
33
|
+
const intervalSeconds = Number(firstNonEmpty(args.intervalSeconds, args.interval, args.everySeconds));
|
|
34
|
+
|
|
35
|
+
if (!prompt.trim()) {
|
|
36
|
+
console.log(JSON.stringify(toolError("prompt/message/task, text, or artifact.text is required")));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!runAt) {
|
|
41
|
+
console.log(JSON.stringify(toolError("args.runAt/at/when or args.delaySeconds/delay/seconds is required")));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const asyncTask = {
|
|
46
|
+
kind: "agent_task",
|
|
47
|
+
runAt,
|
|
48
|
+
payload: { prompt },
|
|
49
|
+
recurrence: Number.isFinite(intervalSeconds) && intervalSeconds > 0
|
|
50
|
+
? { type: "interval", everySeconds: intervalSeconds }
|
|
51
|
+
: null
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
console.log(JSON.stringify(toolOk({ runAt }, {
|
|
55
|
+
status: "scheduled",
|
|
56
|
+
asyncTask
|
|
57
|
+
})));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
if (!args.length || args.includes("--help") || args[0] === "help") {
|
|
62
|
+
printHelp();
|
|
63
|
+
} else if (args[0] === "run") {
|
|
64
|
+
const fileIndex = args.indexOf("--request-file");
|
|
65
|
+
await run(args[fileIndex + 1]);
|
|
66
|
+
} else {
|
|
67
|
+
printHelp();
|
|
68
|
+
}
|