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.
@@ -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 { normalizeArtifactForReasoning } from "../../core/artifacts/normalize-for-reasoning.js";
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 audio 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.`);
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?.kind === "audio" && !transcript && toolResult) {
69
- parts.push(`audioNormalizationResult: ${JSON.stringify(toolResult)}`);
70
- parts.push(`Important: pre-reasoning audio normalization could not be completed, so you do not have a transcript for this voice/audio message.`);
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 audio artifact has already been normalized for reasoning. Use the transcript as the message content.");
118
- } else if (artifact.kind === "audio" && toolResult) {
119
- parts.push(`audioNormalizationResult: ${JSON.stringify(toolResult)}`);
120
- parts.push("Important: pre-reasoning audio normalization could not be completed, so you do not have a transcript for this audio artifact.");
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
- async function collectText(session, prompt) {
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
- await session.prompt(prompt);
153
- unsubscribe();
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", `audio transcribed to artifact ${transcript.id}`);
198
- if (artifact?.kind === "audio" && !transcript) {
199
- logger?.log("telegram", `audio normalization unavailable for chat ${ctx.chat.id}: ${toolResult?.error || toolResult?.missingConfig?.join(", ") || "unknown error"}`);
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
- const text = await collectText(session, prompt);
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
- while (currentPrompt) {
275
- try {
276
- logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
277
- await processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
278
- } finally {
279
- currentCtx = null;
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
- setInterval(async () => {
385
- const tasks = await taskStore.claimDue(10);
386
- for (const task of tasks) {
387
- try {
388
- if (task.kind !== "agent_task" || !task.payload?.chatId || !task.payload?.prompt) {
389
- await taskStore.fail(task.id, `Unsupported task: ${task.kind}`);
390
- continue;
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/audio.ogg", "mimeType": "audio/ogg" },\n "args": {}\n }\n\nConfig at ${getToolConfigPath(toolName)}:\n OPENAI_API_KEY\n MODEL\n`);
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, getToolOutDir } from "../../src/runtime/paths.js";
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 = getToolOutDir(toolName);
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
- }