arisa 3.1.2 → 3.1.6
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/AGENTS.md +59 -14
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +105 -45
- package/src/core/agent/prompt-timeout.js +22 -0
- package/src/core/agent/runtime-context.js +2 -1
- package/src/core/artifacts/normalize-for-reasoning.js +5 -4
- package/src/core/skills/skill-registry.js +71 -0
- package/src/core/tasks/task-store.js +1 -1
- package/src/core/tools/daemon-processes.js +134 -0
- package/src/core/tools/daemon-runtime.js +55 -51
- package/src/core/tools/tool-registry.js +41 -6
- package/src/index.js +42 -6
- package/src/runtime/bootstrap.js +1 -1
- package/src/runtime/create-app.js +15 -2
- package/src/runtime/paths.js +18 -8
- package/src/runtime/service-manager.js +4 -14
- package/src/runtime/tool-process-supervisor.js +110 -0
- package/src/transport/telegram/bot.js +173 -44
- package/src/transport/telegram/media.js +20 -0
- package/tools/openai-transcribe/index.js +1 -1
- package/tools/openai-transcribe/tool.manifest.json +2 -2
- package/tools/openai-tts/index.js +4 -2
- package/docs/async-event-queue-flow.md +0 -68
- package/src/core/agent/project-instructions.js +0 -11
|
@@ -3,7 +3,10 @@ import path from "node:path";
|
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
4
|
import { captureIncomingArtifact } from "./media.js";
|
|
5
5
|
import { renderTelegramHtml } from "./text-format.js";
|
|
6
|
-
import {
|
|
6
|
+
import { withTimeout } from "../../core/agent/prompt-timeout.js";
|
|
7
|
+
import { normalizeArtifactForReasoning, shouldNormalizeArtifactToText } from "../../core/artifacts/normalize-for-reasoning.js";
|
|
8
|
+
|
|
9
|
+
const promptTimeoutMs = 300_000;
|
|
7
10
|
|
|
8
11
|
function quotedMessageSummary(message) {
|
|
9
12
|
if (!message) return [];
|
|
@@ -63,11 +66,11 @@ function buildPrompt({ ctx, artifact, transcript, toolResult }) {
|
|
|
63
66
|
if (transcript) {
|
|
64
67
|
parts.push(`transcriptArtifactId: ${transcript.id}`);
|
|
65
68
|
parts.push(`transcriptText: ${transcript.text}`);
|
|
66
|
-
parts.push(`Important: the incoming
|
|
69
|
+
parts.push(`Important: the incoming media has already been transcribed. Use the transcript as the user message content. Do not answer with a raw transcription unless the user explicitly asked for one.`);
|
|
67
70
|
}
|
|
68
|
-
if (artifact
|
|
69
|
-
parts.push(`
|
|
70
|
-
parts.push(`Important: pre-reasoning
|
|
71
|
+
if (shouldNormalizeArtifactToText(artifact) && !transcript && toolResult) {
|
|
72
|
+
parts.push(`mediaNormalizationResult: ${JSON.stringify(toolResult)}`);
|
|
73
|
+
parts.push(`Important: pre-reasoning media normalization could not be completed, so you do not have a transcript for this audio/video message.`);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
parts.push(`If you need a CLI tool, use list_tools/tool_help/run_tool.`);
|
|
@@ -114,10 +117,10 @@ async function buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, logger
|
|
|
114
117
|
logger?.log("tasks", `artifact ${artifact.id} normalized to ${normalizedArtifact.id}`);
|
|
115
118
|
parts.push(`transcriptArtifactId: ${normalizedArtifact.id}`);
|
|
116
119
|
parts.push(`transcriptText: ${normalizedArtifact.text}`);
|
|
117
|
-
parts.push("Important: the attached
|
|
118
|
-
} else if (artifact
|
|
119
|
-
parts.push(`
|
|
120
|
-
parts.push("Important: pre-reasoning
|
|
120
|
+
parts.push("Important: the attached media artifact has already been normalized for reasoning. Use the transcript as the message content.");
|
|
121
|
+
} else if (shouldNormalizeArtifactToText(artifact) && toolResult) {
|
|
122
|
+
parts.push(`mediaNormalizationResult: ${JSON.stringify(toolResult)}`);
|
|
123
|
+
parts.push("Important: pre-reasoning media normalization could not be completed, so you do not have a transcript for this audio/video artifact.");
|
|
121
124
|
}
|
|
122
125
|
} else {
|
|
123
126
|
parts.push(`artifactId: ${task.payload.artifactId}`);
|
|
@@ -130,6 +133,18 @@ async function buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, logger
|
|
|
130
133
|
return parts.filter(Boolean).join("\n");
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
function buildAsyncEventPrompt(task) {
|
|
137
|
+
return [
|
|
138
|
+
"External event arrived.",
|
|
139
|
+
`taskId: ${task.id}`,
|
|
140
|
+
`chatId: ${task.payload.chatId}`,
|
|
141
|
+
task.payload.prompt ? `event: ${task.payload.prompt}` : null,
|
|
142
|
+
"A polling checker detected this external event. Evaluate it and decide the next action.",
|
|
143
|
+
"If it warrants no action, you may stay silent.",
|
|
144
|
+
"If needed, use tools."
|
|
145
|
+
].filter(Boolean).join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
async function normalizeIncomingArtifact({ artifact, toolRegistry, chatArtifactStore, chatId }) {
|
|
134
149
|
if (!artifact) return { transcript: null, toolResult: null };
|
|
135
150
|
const { normalizedArtifact, toolResult } = await normalizeArtifactForReasoning({
|
|
@@ -142,15 +157,52 @@ async function normalizeIncomingArtifact({ artifact, toolRegistry, chatArtifactS
|
|
|
142
157
|
return { transcript: normalizedArtifact, toolResult };
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
|
|
160
|
+
function sessionEventLogMessage(event) {
|
|
161
|
+
if (event.type === "tool_execution_start") {
|
|
162
|
+
return `tool ${event.toolName} started`;
|
|
163
|
+
}
|
|
164
|
+
if (event.type === "tool_execution_end") {
|
|
165
|
+
return `tool ${event.toolName} ${event.isError ? "failed" : "finished"}`;
|
|
166
|
+
}
|
|
167
|
+
if (event.type === "auto_retry_start") {
|
|
168
|
+
return `auto retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms: ${event.errorMessage}`;
|
|
169
|
+
}
|
|
170
|
+
if (event.type === "auto_retry_end") {
|
|
171
|
+
return event.success
|
|
172
|
+
? `auto retry succeeded after ${event.attempt} attempt(s)`
|
|
173
|
+
: `auto retry failed after ${event.attempt} attempt(s): ${event.finalError || "unknown error"}`;
|
|
174
|
+
}
|
|
175
|
+
if (event.type === "compaction_start") {
|
|
176
|
+
return `compaction started (${event.reason})`;
|
|
177
|
+
}
|
|
178
|
+
if (event.type === "compaction_end") {
|
|
179
|
+
return `compaction ${event.aborted ? "aborted" : "finished"} (${event.reason})`;
|
|
180
|
+
}
|
|
181
|
+
if (event.type === "message_end" && event.message?.stopReason === "error") {
|
|
182
|
+
return `assistant message ended with error: ${event.message.errorMessage || "unknown error"}`;
|
|
183
|
+
}
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function collectText(session, prompt, { logger, chatId } = {}) {
|
|
146
188
|
let text = "";
|
|
147
189
|
const unsubscribe = session.subscribe((event) => {
|
|
148
190
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
149
191
|
text += event.assistantMessageEvent.delta;
|
|
150
192
|
}
|
|
193
|
+
const logMessage = sessionEventLogMessage(event);
|
|
194
|
+
if (logMessage) logger?.log("agent", `chat ${chatId} ${logMessage}`);
|
|
151
195
|
});
|
|
152
|
-
|
|
153
|
-
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await withTimeout(session.prompt(prompt), {
|
|
199
|
+
timeoutMs: promptTimeoutMs,
|
|
200
|
+
label: `Telegram prompt for chat ${chatId}`
|
|
201
|
+
});
|
|
202
|
+
} finally {
|
|
203
|
+
unsubscribe();
|
|
204
|
+
}
|
|
205
|
+
|
|
154
206
|
return text.trim();
|
|
155
207
|
}
|
|
156
208
|
|
|
@@ -170,6 +222,7 @@ async function withTyping(ctx, work) {
|
|
|
170
222
|
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger, webhookUrl, setHttpRequestHandler }) {
|
|
171
223
|
const bot = new Bot(config.telegram.token);
|
|
172
224
|
const perChatState = new Map();
|
|
225
|
+
let taskTimer = null;
|
|
173
226
|
|
|
174
227
|
function getIncomingChatMeta(ctx) {
|
|
175
228
|
return {
|
|
@@ -194,9 +247,9 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
194
247
|
const artifact = await captureIncomingArtifact(ctx, artifactStore);
|
|
195
248
|
if (artifact) logger?.log("telegram", `captured artifact ${artifact.kind}${artifact.id ? ` ${artifact.id}` : ""}`);
|
|
196
249
|
const { transcript, toolResult } = await normalizeIncomingArtifact({ artifact, toolRegistry, chatArtifactStore, chatId });
|
|
197
|
-
if (transcript) logger?.log("telegram", `
|
|
198
|
-
if (artifact
|
|
199
|
-
logger?.log("telegram", `
|
|
250
|
+
if (transcript) logger?.log("telegram", `media transcribed to artifact ${transcript.id}`);
|
|
251
|
+
if (shouldNormalizeArtifactToText(artifact) && !transcript) {
|
|
252
|
+
logger?.log("telegram", `media normalization unavailable for chat ${ctx.chat.id}: ${toolResult?.error || toolResult?.missingConfig?.join(", ") || "unknown error"}`);
|
|
200
253
|
}
|
|
201
254
|
return buildPrompt({ ctx, artifact, transcript, toolResult });
|
|
202
255
|
}
|
|
@@ -240,7 +293,13 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
240
293
|
async function processPromptForChat({ chatId, prompt, ctx = null }) {
|
|
241
294
|
const work = async () => {
|
|
242
295
|
const { session } = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
|
|
243
|
-
|
|
296
|
+
let text = "";
|
|
297
|
+
try {
|
|
298
|
+
text = await collectText(session, prompt, { logger, chatId });
|
|
299
|
+
} catch (error) {
|
|
300
|
+
agentManager.resetSession(chatId);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
244
303
|
if (text) {
|
|
245
304
|
await sendTextReply({
|
|
246
305
|
sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
|
|
@@ -271,12 +330,19 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
271
330
|
let currentPrompt = prompt;
|
|
272
331
|
let currentCtx = ctx;
|
|
273
332
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
333
|
+
try {
|
|
334
|
+
while (currentPrompt) {
|
|
335
|
+
try {
|
|
336
|
+
logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
|
|
337
|
+
await processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
|
|
338
|
+
} catch (error) {
|
|
339
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
340
|
+
logger?.error("telegram", `${label} failed for chat ${chatId}: ${message}`);
|
|
341
|
+
throw error;
|
|
342
|
+
} finally {
|
|
343
|
+
currentCtx = null;
|
|
344
|
+
}
|
|
345
|
+
|
|
280
346
|
if (chatState.nextPrompt) {
|
|
281
347
|
currentPrompt = chatState.nextPrompt;
|
|
282
348
|
chatState.nextPrompt = "";
|
|
@@ -284,9 +350,9 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
284
350
|
currentPrompt = "";
|
|
285
351
|
}
|
|
286
352
|
}
|
|
353
|
+
} finally {
|
|
354
|
+
chatState.processing = false;
|
|
287
355
|
}
|
|
288
|
-
|
|
289
|
-
chatState.processing = false;
|
|
290
356
|
}
|
|
291
357
|
|
|
292
358
|
async function enqueueOrProcess(ctx) {
|
|
@@ -310,6 +376,73 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
310
376
|
});
|
|
311
377
|
}
|
|
312
378
|
|
|
379
|
+
async function dispatchTask(task) {
|
|
380
|
+
const chatId = task.payload?.chatId;
|
|
381
|
+
if (!chatId) {
|
|
382
|
+
await taskStore.fail(task.id, `Task missing chatId: ${task.kind}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (task.kind === "agent_task") {
|
|
387
|
+
if (!task.payload.prompt) {
|
|
388
|
+
await taskStore.fail(task.id, "agent_task missing prompt");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
logger?.log("tasks", `running task ${task.id} for chat ${chatId}`);
|
|
392
|
+
await enqueuePrompt({
|
|
393
|
+
chatId,
|
|
394
|
+
prompt: await buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, logger }),
|
|
395
|
+
label: `scheduled task ${task.id}`
|
|
396
|
+
});
|
|
397
|
+
await taskStore.complete(task.id);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (task.kind === "agent_event") {
|
|
402
|
+
logger?.log("tasks", `agent event ${task.id} for chat ${chatId}`);
|
|
403
|
+
await enqueuePrompt({
|
|
404
|
+
chatId,
|
|
405
|
+
prompt: buildAsyncEventPrompt(task),
|
|
406
|
+
label: `agent event ${task.id}`
|
|
407
|
+
});
|
|
408
|
+
await taskStore.complete(task.id);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (task.kind === "poll_tool") {
|
|
413
|
+
const toolName = task.payload?.toolName;
|
|
414
|
+
if (!toolName) {
|
|
415
|
+
await taskStore.fail(task.id, "poll_tool missing toolName");
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
logger?.log("tasks", `polling tool ${toolName} (task ${task.id}) for chat ${chatId}`);
|
|
419
|
+
try {
|
|
420
|
+
await agentManager.runTool({
|
|
421
|
+
name: toolName,
|
|
422
|
+
request: { args: task.payload.args || {} },
|
|
423
|
+
chatId
|
|
424
|
+
});
|
|
425
|
+
} catch (error) {
|
|
426
|
+
logger?.log("tasks", `poll_tool ${toolName} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
427
|
+
}
|
|
428
|
+
await taskStore.complete(task.id);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await taskStore.fail(task.id, `Unsupported task: ${task.kind}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function dispatchDueTasks() {
|
|
436
|
+
const tasks = await taskStore.claimDue(10);
|
|
437
|
+
for (const task of tasks) {
|
|
438
|
+
try {
|
|
439
|
+
await dispatchTask(task);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
await taskStore.fail(task.id, error instanceof Error ? error.message : String(error));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
313
446
|
async function handleNewCommand(ctx) {
|
|
314
447
|
agentManager.resetSession(ctx.chat.id);
|
|
315
448
|
perChatState.set(ctx.chat.id, { processing: false, nextPrompt: "" });
|
|
@@ -381,26 +514,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
381
514
|
await bot.api.setMyCommands([
|
|
382
515
|
{ command: "new", description: "Start a new chat context" }
|
|
383
516
|
]);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
logger?.log("tasks", `running task ${task.id} for chat ${task.payload.chatId}`);
|
|
393
|
-
await enqueuePrompt({
|
|
394
|
-
chatId: task.payload.chatId,
|
|
395
|
-
prompt: await buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, logger }),
|
|
396
|
-
label: `scheduled task ${task.id}`
|
|
397
|
-
});
|
|
398
|
-
await taskStore.complete(task.id);
|
|
399
|
-
} catch (error) {
|
|
400
|
-
await taskStore.fail(task.id, error instanceof Error ? error.message : String(error));
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}, 1000).unref();
|
|
517
|
+
if (!taskTimer) {
|
|
518
|
+
taskTimer = setInterval(() => {
|
|
519
|
+
dispatchDueTasks().catch((error) => {
|
|
520
|
+
logger?.error("tasks", `dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
521
|
+
});
|
|
522
|
+
}, 1000);
|
|
523
|
+
taskTimer.unref();
|
|
524
|
+
}
|
|
404
525
|
if (webhookUrl && setHttpRequestHandler) {
|
|
405
526
|
const webhookPath = `/telegram-${config.telegram.token.slice(-8)}`;
|
|
406
527
|
const handleUpdate = webhookCallback(bot, "http", {
|
|
@@ -421,6 +542,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
421
542
|
logger?.log("telegram", "bot polling started");
|
|
422
543
|
await bot.start({ drop_pending_updates: true });
|
|
423
544
|
}
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
async stop() {
|
|
548
|
+
if (taskTimer) clearInterval(taskTimer);
|
|
549
|
+
taskTimer = null;
|
|
550
|
+
try {
|
|
551
|
+
bot.stop();
|
|
552
|
+
} catch {}
|
|
424
553
|
}
|
|
425
554
|
};
|
|
426
555
|
}
|
|
@@ -33,6 +33,26 @@ export async function captureIncomingArtifact(ctx, artifactStore) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
if (ctx.message?.video) {
|
|
37
|
+
const video = ctx.message.video;
|
|
38
|
+
const fileName = video.file_name || `${chatId}-${ctx.msg.message_id}.mp4`;
|
|
39
|
+
const content = await downloadToBuffer(ctx, video.file_id);
|
|
40
|
+
return store.createGeneratedFile({
|
|
41
|
+
fileName,
|
|
42
|
+
content,
|
|
43
|
+
kind: "video",
|
|
44
|
+
mimeType: video.mime_type || "video/mp4",
|
|
45
|
+
source: baseSource,
|
|
46
|
+
metadata: {
|
|
47
|
+
duration: video.duration,
|
|
48
|
+
width: video.width,
|
|
49
|
+
height: video.height,
|
|
50
|
+
fileSize: video.file_size,
|
|
51
|
+
...incomingCaptionMetadata(ctx)
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
if (ctx.message?.document) {
|
|
37
57
|
const fileName = ctx.message.document.file_name || `${chatId}-${ctx.msg.message_id}`;
|
|
38
58
|
const content = await downloadToBuffer(ctx, ctx.message.document.file_id);
|
|
@@ -9,7 +9,7 @@ const toolName = "openai-transcribe";
|
|
|
9
9
|
const config = await loadToolConfig(toolName, defaults);
|
|
10
10
|
|
|
11
11
|
function printHelp() {
|
|
12
|
-
console.log(`openai-transcribe\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "artifact": { "path": "/abs/
|
|
12
|
+
console.log(`openai-transcribe\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "artifact": { "path": "/abs/media.ogg", "mimeType": "audio/ogg" },\n "args": {}\n }\n\nConfig at ${getToolConfigPath(toolName)}:\n OPENAI_API_KEY\n MODEL\n`);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async function run(requestFile) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openai-transcribe",
|
|
3
|
-
"description": "Transcribe audio files with OpenAI audio transcription API.",
|
|
3
|
+
"description": "Transcribe audio files and video audio tracks with OpenAI audio transcription API.",
|
|
4
4
|
"entry": "index.js",
|
|
5
|
-
"input": ["audio/ogg", "audio/mpeg", "audio/wav", "audio/mp4"],
|
|
5
|
+
"input": ["audio/ogg", "audio/mpeg", "audio/wav", "audio/mp4", "video/mp4"],
|
|
6
6
|
"output": ["text/plain"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"OPENAI_API_KEY": {
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import defaults from "./config.js";
|
|
4
4
|
import { loadToolConfig } from "../../src/core/tools/tool-config.js";
|
|
5
5
|
import { toolError, toolNeedsConfig, toolOk } from "../../src/core/tools/tool-result.js";
|
|
6
|
-
import { getToolConfigPath,
|
|
6
|
+
import { getChatToolTmpDir, getToolConfigPath, getToolTmpDir } from "../../src/runtime/paths.js";
|
|
7
7
|
|
|
8
8
|
const toolName = "openai-tts";
|
|
9
9
|
const config = await loadToolConfig(toolName, defaults);
|
|
@@ -49,7 +49,9 @@ async function run(requestFile) {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const outDir =
|
|
52
|
+
const outDir = request.chatId != null
|
|
53
|
+
? getChatToolTmpDir(request.chatId, toolName)
|
|
54
|
+
: getToolTmpDir(toolName);
|
|
53
55
|
await mkdir(outDir, { recursive: true });
|
|
54
56
|
const filePath = path.join(outDir, `speech-${Date.now()}.ogg`);
|
|
55
57
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# Flow genérico de eventos asíncronos para tools
|
|
2
|
-
|
|
3
|
-
> Estado: propuesta / no implementado. Guardado como referencia.
|
|
4
|
-
> La implementación actual (timer) se mantiene; este documento describe una evolución posible.
|
|
5
|
-
|
|
6
|
-
## Problema
|
|
7
|
-
|
|
8
|
-
Hoy la única re-entrada asíncrona al agente es por tiempo: una tool devuelve `asyncTask` con `runAt` y el poller de 1s en `src/transport/telegram/bot.js` lo dispara como prompt. Eso obliga a resolver con timer (polling crudo, latencia fija, re-spawn de la tool y un turno completo del agente en cada chequeo). Falta una **cola de eventos entrantes** que despierte al agente solo cuando hay algo que evaluar.
|
|
9
|
-
|
|
10
|
-
## Solución (polling ordenado por cola, reusando TaskStore)
|
|
11
|
-
|
|
12
|
-
Dos nuevos `kind` de tarea, drenados por el mismo poller hacia el mismo `enqueuePrompt`:
|
|
13
|
-
|
|
14
|
-
- `poll_tool`: tarea recurrente que el poller **ejecuta directamente como tool** (no gasta turno del agente). El checker mantiene su propio cursor de estado en su config/tmp por chat. Si hay novedad, emite un `agent_event`.
|
|
15
|
-
- `agent_event`: evento entrante que se dispara de inmediato. El poller lo entrega como prompt para que Pi lo evalúe y decida.
|
|
16
|
-
|
|
17
|
-
```mermaid
|
|
18
|
-
flowchart LR
|
|
19
|
-
Tool[Tool run normal] -->|asyncTask poll_tool| TS[TaskStore]
|
|
20
|
-
TS --> Poller[1s poller dispatcher]
|
|
21
|
-
Poller -->|kind poll_tool| Run[agentManager.runTool checker]
|
|
22
|
-
Run -->|si hay novedad: asyncTask agent_event| TS
|
|
23
|
-
Poller -->|kind agent_event| EP[enqueuePrompt]
|
|
24
|
-
Poller -->|kind agent_task| EP
|
|
25
|
-
EP --> Pi[Pi evalua y decide]
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Cambios
|
|
29
|
-
|
|
30
|
-
### 1. TaskStore: eventos/polls sin hora se disparan ya
|
|
31
|
-
|
|
32
|
-
`src/core/tasks/task-store.js` - en `normalizeTask`, default `runAt` a `now` cuando no viene (los `agent_event` y el primer disparo de `poll_tool` deben ser inmediatos; `computeNextRunAt` ya reprograma `poll_tool` por su `recurrence`). Cambio de una línea, no rompe `agent_task` (siempre trae `runAt`).
|
|
33
|
-
|
|
34
|
-
### 2. AgentManager: extraer "run + materializar" (DRY)
|
|
35
|
-
|
|
36
|
-
`src/core/agent/agent-manager.js` - hoy el `execute` de `run_tool` (líneas ~184-242) hace: correr la tool, convertir `output.text`/`output.filePath` en artifacts y mandar `asyncTask(s)` al `TaskStore` con el `chatId`. Extraer eso a un método reusable `runTool({ name, request, chatId })`. El Pi tool `run_tool` pasa a llamarlo. Así el poller puede correr tools con la **misma** lógica de materialización (incluido el alta de `agent_event` que emita el checker).
|
|
37
|
-
|
|
38
|
-
### 3. Poller -> dispatcher por kind
|
|
39
|
-
|
|
40
|
-
`src/transport/telegram/bot.js` - reemplazar el handler de un solo kind dentro del `setInterval` (líneas ~361-380) por un dispatcher:
|
|
41
|
-
|
|
42
|
-
- `agent_task` -> `enqueuePrompt(buildAsyncTaskPrompt(task))` + `complete` (igual que hoy).
|
|
43
|
-
- `agent_event` -> `enqueuePrompt(buildAsyncEventPrompt(task))` + `complete`.
|
|
44
|
-
- `poll_tool` -> `agentManager.runTool({ name: task.payload.toolName, request: { args: task.payload.args || {} }, chatId })`; los `agent_event` que emita el checker quedan encolados para el próximo tick; luego `complete` (la `recurrence` reprograma el poll). Si la tool falla: log + `complete` para no matar el poll.
|
|
45
|
-
|
|
46
|
-
Agregar `buildAsyncEventPrompt(task)` junto a `buildAsyncTaskPrompt` (línea ~82), con framing de "llegó un evento externo, evalualo y decidí la próxima acción". Si el branch queda denso, extraer `dispatchDueTasks(...)` a una función para mantener `bot.js` como transporte.
|
|
47
|
-
|
|
48
|
-
### 4. Documentar el flow
|
|
49
|
-
|
|
50
|
-
`AGENTS.md` - sección nueva (en inglés) explicando: cómo una tool arma su auto-polling devolviendo un `asyncTask` kind `poll_tool` con `recurrence`, cómo emite novedades con `asyncTask` kind `agent_event`, que el checker guarda su cursor en su config/tmp por chat, y que el agente razona sobre el `agent_event` para decidir. `list_scheduled_tasks`/`cancel_scheduled_task` ya sirven (son kind-agnostic) para ver/cancelar polls.
|
|
51
|
-
|
|
52
|
-
## Contrato del checker tool (sin nuevas Pi tools)
|
|
53
|
-
|
|
54
|
-
Todo pasa por el campo `asyncTasks` que el pipeline ya soporta:
|
|
55
|
-
|
|
56
|
-
- Arranque del poll (desde el `run` de cualquier tool): `asyncTasks: [{ kind: "poll_tool", payload: { toolName, args }, recurrence: { type: "interval", everySeconds: N } }]`.
|
|
57
|
-
- Novedad (desde el `run` del checker): `asyncTasks: [{ kind: "agent_event", payload: { prompt: "<contenido a evaluar>" } }]`.
|
|
58
|
-
|
|
59
|
-
## No-goals (por ahora)
|
|
60
|
-
|
|
61
|
-
- No se agrega listener persistente (`node index.js listen`) ni proceso de fondo con IPC.
|
|
62
|
-
- No se agrega endpoint HTTP entrante para eventos.
|
|
63
|
-
- No se resuelve el caso de conexión sostenida (tipo cliente logueado): los checkers son one-shot y persisten su cursor entre corridas.
|
|
64
|
-
|
|
65
|
-
## Alternativas consideradas (descartadas para esta versión)
|
|
66
|
-
|
|
67
|
-
- **Listener tools**: la tool corre como proceso de larga duración (`node index.js listen`) y emite eventos por stdout que Arisa drena a la cola. Más general y realtime, pero agrega ciclo de vida de proceso a la service e IPC.
|
|
68
|
-
- **Webhook entrante**: Arisa expone un endpoint HTTP interno donde sistemas externos hacen POST de eventos. Bueno para callbacks; no sirve para los que requieren sostener una conexión.
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
|
|
4
|
-
const instructionsPath = fileURLToPath(new URL("../../../AGENTS.md", import.meta.url));
|
|
5
|
-
let cachedInstructions = null;
|
|
6
|
-
|
|
7
|
-
export async function loadProjectInstructions() {
|
|
8
|
-
if (cachedInstructions !== null) return cachedInstructions;
|
|
9
|
-
cachedInstructions = await readFile(instructionsPath, "utf8");
|
|
10
|
-
return cachedInstructions;
|
|
11
|
-
}
|