arisa 4.1.8 → 4.1.10

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 CHANGED
@@ -121,7 +121,9 @@ To run a pipe, the agent should:
121
121
  Example manual pipe:
122
122
  1. `run_tool(openai-transcribe, artifact audio)`
123
123
  2. take the returned text `artifactId`
124
- 3. `run_tool(openai-tts, artifact text)` or `send_media_reply(text)`
124
+ 3. `run_tool(openai-tts, artifact text)` to generate audio, then `send_artifact(artifactId)` to deliver it (or `run_tool(openai-tts, artifact text, deliver: true)` to generate and send in one step)
125
+
126
+ Delivery is generic: any `run_tool` output that produces a file becomes an artifact, and `send_artifact(artifactId)` delivers it to the chat. The delivery method and caption are derived from the artifact (its `delivery` hint, `kind`, and stored name); internal local paths are never exposed. Tools declare their delivery intent by returning `delivery: { method }` in their output; they do not deliver to the transport themselves. As a shortcut, `run_tool` accepts `deliver: true` to generate and deliver in a single step; use it only when the user should receive the file now, not for intermediate pipe steps.
125
127
 
126
128
  ## Async event queue flow
127
129
  Beyond time-based scheduling, tools can drive an event queue that wakes the agent only when there is something to evaluate. Everything goes through the `asyncTask` (single) or `asyncTasks` (array) field the pipeline already supports; no new Pi tools are needed. The 1s poller drains tasks by `kind`:
@@ -195,6 +197,8 @@ Tools may declare skills in `tool.manifest.json`:
195
197
 
196
198
  The tool registry resolves these from the installed skills directory and injects them into the tool request as `skills`. `list_tools` exposes the hints and `tool_help` shows their resolution status. Skills are guidance for the agent/tool; they are not separate runtime dependencies.
197
199
 
200
+ If a tool executes deterministic logic over skill content, bundle the required skill assets inside the tool package and treat injected `skills` only as an optional override. A catalog tool must still work when the hinted skill is not installed on the target Arisa instance.
201
+
198
202
  ## Safety
199
203
  - Prefer tool manifests and CLI help over assumptions.
200
204
  - Keep config and runtime data inside the user runtime area through the path helpers above.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "4.1.8",
3
+ "version": "4.1.10",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -19,7 +19,7 @@ const arisaToolNames = [
19
19
  "list_scheduled_tasks",
20
20
  "cancel_scheduled_task",
21
21
  "cancel_all_scheduled_tasks",
22
- "send_media_reply"
22
+ "send_artifact"
23
23
  ];
24
24
 
25
25
  function isLocalBaseUrl(value) {
@@ -55,30 +55,31 @@ async function promptAndThrowOnAssistantError(session, prompt) {
55
55
  }
56
56
  }
57
57
 
58
- function mimeMatches(pattern, mimeType = "") {
59
- if (!pattern || !mimeType) return false;
60
- if (pattern === mimeType) return true;
61
- if (pattern.endsWith("/*")) return mimeType.startsWith(`${pattern.slice(0, -2)}/`);
62
- return false;
58
+ function inferDeliveryMethod(artifact) {
59
+ if (artifact.kind === "audio" || (artifact.mimeType || "").startsWith("audio/")) return "audio";
60
+ return "document";
63
61
  }
64
62
 
65
- function toolSupportsTextInput(tool) {
66
- return (tool.input || []).some((input) => mimeMatches(input, "text/plain"));
63
+ function containsAbsolutePath(value) {
64
+ if (typeof value !== "string") return false;
65
+ return /(^|\s)(\/[^\s]|[A-Za-z]:[\\/])/.test(value);
67
66
  }
68
67
 
69
- function toolProducesAudio(tool) {
70
- return (tool.output || []).some((output) => mimeMatches(output, "audio/ogg") || mimeMatches(output, "audio/*"));
68
+ function resolveMediaCaption({ caption, output, method }) {
69
+ if (caption && !containsAbsolutePath(caption)) return caption;
70
+ if (method === "document" && output?.fileName) return output.fileName;
71
+ if (output?.text && !containsAbsolutePath(output.text)) return output.text;
72
+ if (output?.fileName) return output.fileName;
73
+ return undefined;
71
74
  }
72
75
 
73
- function looksLikeTextToSpeechTool(tool) {
74
- return /tts|text.?to.?speech|speech.?audio|speech/i.test(`${tool.name} ${tool.description || ""}`);
75
- }
76
-
77
- function selectMediaReplyTool(toolRegistry) {
78
- const tools = toolRegistry.list()
79
- .filter(toolSupportsTextInput)
80
- .filter(toolProducesAudio);
81
- return tools.find(looksLikeTextToSpeechTool) || tools[0] || null;
76
+ async function deliverArtifactToChat({ artifact, telegram, caption, method, logger }) {
77
+ const resolvedMethod = method || artifact.metadata?.delivery?.method || inferDeliveryMethod(artifact);
78
+ const fileName = path.basename(artifact.path);
79
+ const resolvedCaption = resolveMediaCaption({ caption, output: { fileName }, method: resolvedMethod });
80
+ logger?.log("agent", `deliver artifact ${artifact.id} as ${resolvedMethod}`);
81
+ await telegram.sendMedia(artifact.path, { method: resolvedMethod, caption: resolvedCaption, filename: fileName });
82
+ return { method: resolvedMethod, fileName, artifactId: artifact.id };
82
83
  }
83
84
 
84
85
  async function assertDirectory(dir, label) {
@@ -238,7 +239,7 @@ export class AgentManager {
238
239
  kind: result.output.kind || "file",
239
240
  mimeType: result.output.mimeType || "application/octet-stream",
240
241
  source: { type: "tool", toolName: name },
241
- metadata: { tool: name }
242
+ metadata: { tool: name, delivery: result.output.delivery }
242
243
  });
243
244
  result.output.artifactId = generated.id;
244
245
  await unlink(result.output.filePath).catch(() => {});
@@ -337,12 +338,13 @@ export class AgentManager {
337
338
  defineTool({
338
339
  name: "run_tool",
339
340
  label: "Run tool",
340
- description: "Run a CLI tool using text input or an artifactId. Inspect the returned status/resolution fields. If a tool reports missing config, ask the user naturally, use set_tool_config, and retry.",
341
+ description: "Run a CLI tool using text input or an artifactId. Inspect the returned status/resolution fields. If a tool reports missing config, ask the user naturally, use set_tool_config, and retry. Set `deliver: true` to also send the generated file to the chat in one step (only when you want the user to receive it now, not for intermediate pipe steps).",
341
342
  parameters: Type.Object({
342
343
  name: Type.String(),
343
344
  artifactId: Type.Optional(Type.String()),
344
345
  text: Type.Optional(Type.String()),
345
- args: Type.Optional(Type.Record(Type.String(), Type.String()))
346
+ args: Type.Optional(Type.Record(Type.String(), Type.String())),
347
+ deliver: Type.Optional(Type.Boolean())
346
348
  }),
347
349
  execute: async (_id, params) => {
348
350
  let artifact = null;
@@ -362,6 +364,13 @@ export class AgentManager {
362
364
  chatId
363
365
  });
364
366
 
367
+ if (params.deliver && result.output?.artifactId) {
368
+ const generated = await chatArtifactStore.get(result.output.artifactId);
369
+ if (generated?.path) {
370
+ result.sent = await deliverArtifactToChat({ artifact: generated, telegram, logger: this.logger });
371
+ }
372
+ }
373
+
365
374
  return {
366
375
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
367
376
  details: result
@@ -417,12 +426,12 @@ export class AgentManager {
417
426
  }
418
427
  }),
419
428
  defineTool({
420
- name: "send_media_reply",
421
- label: "Send media reply",
422
- description: "Run a CLI tool that generates a file and send it to the current Telegram chat using the tool's delivery hint or an explicit method.",
429
+ name: "send_artifact",
430
+ label: "Send artifact",
431
+ description: "Deliver an existing chat artifact to the current Telegram chat. Pass the `artifactId` returned by run_tool or from an inbound file. The delivery method, caption, and filename are derived from the artifact (its delivery hint, kind, and stored name); internal local paths are never exposed. Set `caption` for a visible label, or `method` to override the delivery method. The artifact is not deleted.",
423
432
  parameters: Type.Object({
424
- text: Type.String(),
425
- toolName: Type.Optional(Type.String()),
433
+ artifactId: Type.String(),
434
+ caption: Type.Optional(Type.String()),
426
435
  method: Type.Optional(Type.Union([
427
436
  Type.Literal("voice"),
428
437
  Type.Literal("audio"),
@@ -430,36 +439,25 @@ export class AgentManager {
430
439
  ]))
431
440
  }),
432
441
  execute: async (_id, params) => {
433
- await this.toolRegistry.load();
434
- const selectedTool = params.toolName
435
- ? this.toolRegistry.get(params.toolName)
436
- : selectMediaReplyTool(this.toolRegistry);
437
- if (!selectedTool) {
438
- const result = {
439
- ok: false,
440
- status: "failed",
441
- error: params.toolName
442
- ? `Tool not found: ${params.toolName}`
443
- : "No registered text-to-speech tool can generate an audio reply."
444
- };
442
+ const artifact = await chatArtifactStore.get(params.artifactId);
443
+ if (!artifact) {
444
+ const result = { ok: false, status: "failed", error: `Artifact not found: ${params.artifactId}` };
445
445
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
446
446
  }
447
- const toolName = selectedTool.name;
448
- this.logger?.log("agent", `send_media_reply via ${toolName}`);
449
- const result = await this.toolRegistry.run({
450
- name: toolName,
451
- request: { text: params.text, args: {} },
452
- chatId
453
- });
454
- if (!result.ok || !result.output?.filePath) {
447
+ if (!artifact.path) {
448
+ const result = { ok: false, status: "failed", error: `Artifact ${params.artifactId} has no file to deliver.` };
455
449
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
456
450
  }
457
- const method = params.method || result.output?.delivery?.method || "audio";
458
- await telegram.sendMedia(result.output.filePath, { method, caption: params.text });
459
- await unlink(result.output.filePath).catch(() => {});
451
+ const sent = await deliverArtifactToChat({
452
+ artifact,
453
+ telegram,
454
+ caption: params.caption,
455
+ method: params.method,
456
+ logger: this.logger
457
+ });
460
458
  return {
461
- content: [{ type: "text", text: `Media sent to Telegram as ${method}.` }],
462
- details: { ...result, sent: { method } }
459
+ content: [{ type: "text", text: `Media sent to Telegram as ${sent.method}.` }],
460
+ details: { ok: true, sent }
463
461
  };
464
462
  }
465
463
  })
@@ -77,7 +77,7 @@ function buildPrompt({ ctx, artifact, transcript, toolResult }) {
77
77
  parts.push(`Use read/write/edit for file work in the active workspace, bash for bash-compatible commands, and system_shell for native system commands such as PowerShell on Windows.`);
78
78
  parts.push(`If you need an Arisa modular CLI tool, use list_tools/tool_help/run_tool.`);
79
79
  parts.push(`If a tool config is missing, ask the user naturally and then use set_tool_config.`);
80
- parts.push(`If the user wants a generated media reply, use send_media_reply.`);
80
+ parts.push(`To deliver a file to the chat: run_tool with deliver:true to generate and send in one step, or send_artifact with an existing artifactId (e.g. an inbound file).`);
81
81
  return parts.join("\n");
82
82
  }
83
83
 
@@ -426,9 +426,9 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
426
426
 
427
427
  function createTelegramSessionBridge(chatId) {
428
428
  return {
429
- sendMedia: async (filePath, { method = "audio", caption } = {}) => {
429
+ sendMedia: async (filePath, { method = "audio", caption, filename } = {}) => {
430
430
  logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
431
- const input = new InputFile(filePath);
431
+ const input = new InputFile(filePath, filename || undefined);
432
432
  if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
433
433
  if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
434
434
  return bot.api.sendAudio(chatId, input, { caption });