arisa 3.0.11 → 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 +68 -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 +126 -47
- 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
|
}
|
|
@@ -21,6 +22,10 @@ export class AgentManager {
|
|
|
21
22
|
this.sessions.clear();
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
resetSession(chatId) {
|
|
26
|
+
this.sessions.delete(chatId);
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
async validatePiAgent() {
|
|
25
30
|
this.logger?.log("agent", "validating Pi session");
|
|
26
31
|
const { authStorage, modelRegistry } = createPiRuntime({
|
|
@@ -61,7 +66,7 @@ export class AgentManager {
|
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
this.logger?.log("agent", `creating session for chat ${chatId}`);
|
|
64
|
-
const customTools = this.createTools(telegram);
|
|
69
|
+
const customTools = this.createTools(telegram, chatId);
|
|
65
70
|
const { session } = await createAgentSession({
|
|
66
71
|
cwd: arisaInstallDir,
|
|
67
72
|
agentDir: arisaHomeDir,
|
|
@@ -83,7 +88,7 @@ export class AgentManager {
|
|
|
83
88
|
return ctx;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
createTools(telegram) {
|
|
91
|
+
createTools(telegram, chatId) {
|
|
87
92
|
return [
|
|
88
93
|
defineTool({
|
|
89
94
|
name: "list_tools",
|
|
@@ -171,12 +176,72 @@ export class AgentManager {
|
|
|
171
176
|
await unlink(result.output.filePath).catch(() => {});
|
|
172
177
|
}
|
|
173
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
|
+
|
|
174
191
|
return {
|
|
175
192
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
176
193
|
details: result
|
|
177
194
|
};
|
|
178
195
|
}
|
|
179
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
|
+
}),
|
|
180
245
|
defineTool({
|
|
181
246
|
name: "send_media_reply",
|
|
182
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);
|
|
@@ -240,10 +305,19 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
240
305
|
return ctx.reply(auth.firstTime ? "This chat is now authorized for Arisa." : "Arisa is ready.");
|
|
241
306
|
});
|
|
242
307
|
|
|
308
|
+
bot.command("new", async (ctx) => {
|
|
309
|
+
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
310
|
+
if (!auth.ok) return;
|
|
311
|
+
await handleNewCommand(ctx);
|
|
312
|
+
});
|
|
313
|
+
|
|
243
314
|
bot.on("message", async (ctx) => {
|
|
244
315
|
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
245
316
|
if (!auth.ok) return;
|
|
246
317
|
|
|
318
|
+
const command = getTelegramCommand(ctx);
|
|
319
|
+
if (command) return;
|
|
320
|
+
|
|
247
321
|
try {
|
|
248
322
|
await enqueueOrProcess(ctx);
|
|
249
323
|
} catch (error) {
|
|
@@ -261,16 +335,6 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
261
335
|
try {
|
|
262
336
|
logger?.log("telegram", `generating startup message for chat ${chatId}`);
|
|
263
337
|
const chatMeta = config.telegram.chatMeta[chatId] || {};
|
|
264
|
-
const telegram = {
|
|
265
|
-
sendMedia: async (filePath, { method = "audio", caption } = {}) => {
|
|
266
|
-
logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
|
|
267
|
-
const input = new InputFile(filePath);
|
|
268
|
-
if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
|
|
269
|
-
if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
|
|
270
|
-
return bot.api.sendAudio(chatId, input, { caption });
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
const { session } = await agentManager.getSessionContext(chatId, telegram);
|
|
274
338
|
const welcomePrompt = [
|
|
275
339
|
"System event: Arisa has just started.",
|
|
276
340
|
`chatId: ${chatId}`,
|
|
@@ -282,21 +346,36 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
|
|
|
282
346
|
"Use the user's Telegram language when possible.",
|
|
283
347
|
"Do not mention internal implementation details."
|
|
284
348
|
].filter(Boolean).join("\n");
|
|
285
|
-
|
|
286
|
-
if (text) {
|
|
287
|
-
await sendTextReply({
|
|
288
|
-
sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
|
|
289
|
-
sendDocument: (file, extra) => bot.api.sendDocument(chatId, file, extra),
|
|
290
|
-
chatId,
|
|
291
|
-
text
|
|
292
|
-
});
|
|
293
|
-
}
|
|
349
|
+
await enqueuePrompt({ chatId, prompt: welcomePrompt, label: "startup message" });
|
|
294
350
|
} catch (error) {
|
|
295
351
|
logger?.log("telegram", `startup message failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
296
352
|
}
|
|
297
353
|
}
|
|
354
|
+
await bot.api.setMyCommands([
|
|
355
|
+
{ command: "new", description: "Start a new chat context" }
|
|
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();
|
|
298
377
|
logger?.log("telegram", "bot polling started");
|
|
299
|
-
await bot.start();
|
|
378
|
+
await bot.start({ drop_pending_updates: true });
|
|
300
379
|
}
|
|
301
380
|
};
|
|
302
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
|
+
}
|