arisa 4.0.20 → 4.0.24

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
@@ -1,12 +1,16 @@
1
1
  # Arisa AGENTS
2
2
 
3
- ## Architecture
3
+ ## Core boundaries
4
+ Arisa core owns transport, sessions, artifacts, and tool orchestration:
4
5
  - Telegram transport handles inbound and outbound messaging.
5
6
  - Pi Agent keeps one session per authorized chat.
6
7
  - Incoming messages and files (text, voice, photo, document) and generated files become artifacts.
7
- - A tool registry handles tool discovery, help lookup, config writes, and execution.
8
- - Tools are isolated and each one has its own manifest, entrypoint, and config defaults.
9
- - No tools ship with the core. All installed tools live under `~/.arisa/tools/<toolName>`; the install directory of Arisa (your working directory) contains only the core. Never create or install tools inside the install directory.
8
+ - The tool registry handles tool discovery, help lookup, config writes, and execution.
9
+ - Tools are isolated packages with their own manifest, entrypoint, and config defaults.
10
+ - No tools ship with the core; installed tools live under `~/.arisa/tools/<toolName>`.
11
+ - The Arisa install directory (your working directory) contains only the core. Never create or install tools inside it.
12
+
13
+ New capabilities belong in tools by default. Solve requests by creating or editing a tool under `~/.arisa/tools/<toolName>`. Modifying core is the last resort: do it only after confirming the capability cannot be delivered through the tool architecture, explaining why the core change is unavoidable, and receiving explicit user approval.
10
14
 
11
15
  ## Runtime directory rules
12
16
  Do not build runtime paths by hand. Use `src/runtime/paths.js`:
@@ -33,9 +37,9 @@ Each tool declares in `tool.manifest.json`:
33
37
  - `skillHints`: optional skills to apply when using or editing the tool
34
38
 
35
39
  ## Tool-to-Arisa IPC
36
- Arisa does not mount tool-provided web routes in core. If a tool needs to expose a web UI or HTTP endpoint, the tool owns that server, usually through the shared daemon runtime. The tool's server handles its own requests and uses Arisa IPC when it needs artifacts, tasks, agent events, or runtime paths.
40
+ Tools that expose a web UI or HTTP endpoint own that server, usually through the shared daemon runtime; Arisa core does not mount tool routes or proxies. Use Arisa IPC when a tool needs registered tools, artifacts, tasks, agent events, or runtime paths.
37
41
 
38
- Installed tools can import the IPC client through `ARISA_PACKAGE_DIR`:
42
+ Import the IPC client through `ARISA_PACKAGE_DIR`:
39
43
 
40
44
  ```js
41
45
  import path from "node:path";
@@ -46,9 +50,14 @@ const { createArisaClient } = await importCore("core/tools/ipc-client.js");
46
50
 
47
51
  const arisa = createArisaClient({ toolName: "example-tool", chatId });
48
52
  await arisa.artifacts.createText({ text: "hello" });
53
+ const result = await arisa.tools.run({
54
+ name: "strudel-agent",
55
+ text: prompt,
56
+ args: { bpm, tags, currentCode }
57
+ }, { timeoutMs: 120_000 });
49
58
  ```
50
59
 
51
- The IPC channel is a local socket under `~/.arisa/state`. Every request must include `toolName`; chat-scoped capabilities also require `chatId`. Exposed capabilities are explicit: artifacts (`createText`, `listRecent`, `get`), tasks (`add`, `list`, `cancel`), agent events (`enqueueEvent`), and runtime paths (`getChatToolStateDir`, `getToolStateDir`, `getChatToolTmpDir`, `getToolTmpDir`, `getChatArtifactsDir`). Do not add raw access to `agentManager`, `taskStore`, `artifactStore`, or `toolRegistry`.
60
+ The IPC channel is a local socket under `~/.arisa/state`. Every request must include `toolName`; chat-scoped capabilities also require `chatId`. Exposed capabilities are explicit: tools (`run`), artifacts (`createText`, `listRecent`, `get`), tasks (`add`, `list`, `cancel`), agent events (`enqueueEvent`), and runtime paths (`getChatToolStateDir`, `getToolStateDir`, `getChatToolTmpDir`, `getToolTmpDir`, `getChatArtifactsDir`). Do not expose raw `agentManager`, `taskStore`, `artifactStore`, or `toolRegistry` access.
52
61
 
53
62
  ## Conceptual pipe model
54
63
  There are two different moments where pipes can happen:
@@ -143,43 +152,31 @@ If `run_tool` returns `missingConfig`, the agent should:
143
152
 
144
153
  Do not assume a rigid question/answer protocol. Continue the conversation naturally and infer the config value from the user reply when possible.
145
154
 
146
- ## Official tool catalog
147
- The official tool catalog lives at `https://github.com/clasen/Arisa/tree/main/tools`. Each subdirectory is an installable tool following the standard tool contract.
148
-
149
- When a capability is missing, check the catalog before building anything:
150
- 1. List the catalog: `curl -s https://api.github.com/repos/clasen/Arisa/contents/tools`.
151
- 2. Read the manifest of any candidate: `https://raw.githubusercontent.com/clasen/Arisa/main/tools/<name>/tool.manifest.json` (the `description`, `input`, and `output` fields tell you whether it solves the need). Also read the catalog `README.md` to check the tool's install footprint.
152
- 3. If a catalog tool solves the problem, decide by install footprint (the `Install footprint` column in the catalog `README.md`):
153
- - **Low footprint** (no extra npm dependencies, no external binaries): install it and resolve the user's request in the same turn, without asking first. Favor autonomy here.
154
- - **Medium/High footprint** (heavy dependency trees, external binaries, or interactive setup such as a login): propose it to the user, say which tool it is and what it does, and wait for their confirmation in the chat before installing.
155
- - A missing config secret (for example an API key) is handled by the missing-config flow and is not, on its own, a reason to ask before installing.
156
- 4. Only when nothing in the catalog fits, fall back to creating a new tool.
157
-
158
- ### Installing a tool
159
- After deciding to install (low footprint), after the user confirms, or when the user directly asks to install a tool from any source they choose:
160
- 1. Download the tool directory into `~/.arisa/tools/<name>`. For the official catalog, clone shallowly and copy the subdirectory, for example:
161
- `git clone --depth 1 https://github.com/clasen/Arisa /tmp/arisa-catalog && cp -R /tmp/arisa-catalog/tools/<name> ~/.arisa/tools/<name>`.
162
- 2. Install dependencies inside the tool directory (`pnpm install`, fall back to `npm install`).
163
- 3. Run it through `run_tool`; the registry picks up new tools automatically and exposes the Arisa package root through `ARISA_PACKAGE_DIR`. If it returns `missingConfig`, follow the missing config flow.
164
-
165
- ## Tool creation
166
- Reason in terms of capabilities, not tool names.
155
+ ## Capability resolution
156
+ Reason in terms of capabilities, not tool names. Do not stop at "I cannot do that" when the task is realistically implementable through the tool architecture.
167
157
 
168
158
  When the user asks for something new:
169
- 1. check whether an existing registered tool can already satisfy the task
170
- 2. also check whether the task can be satisfied indirectly through an existing capability
171
- 3. check whether a tool in the official catalog satisfies the task and propose installing it
172
- 4. only propose creating a new tool when the needed capability is truly missing
173
-
174
- Do not stop at "I cannot do that" when the task is realistically implementable through the tool architecture.
175
- The default attitude is:
176
- - identify that no current tool satisfies the request
177
- - state that the missing capability can be added
178
- - propose installing from the official catalog, or propose or start creating the needed tool
159
+ 1. check whether an existing registered tool, or an indirect use of one, can satisfy the task
160
+ 2. check the official catalog at `https://github.com/clasen/Arisa/tree/main/tools` before building anything
161
+ 3. install from the catalog when it fits, or create a new tool only when the needed capability is truly missing
162
+
163
+ To evaluate the official catalog:
164
+ 1. List it: `curl -s https://api.github.com/repos/clasen/Arisa/contents/tools`.
165
+ 2. Read each candidate manifest at `https://raw.githubusercontent.com/clasen/Arisa/main/tools/<name>/tool.manifest.json`; `description`, `input`, and `output` show whether it solves the need.
166
+ 3. Read the catalog `README.md` and use its `Install footprint` column:
167
+ - **Low footprint**: install it and resolve the request in the same turn, without asking first.
168
+ - **Medium/High footprint**: tell the user which tool it is, what it does, and wait for confirmation before installing.
169
+ - Missing config secrets are handled by the missing-config flow and are not, on their own, a reason to ask before installing.
170
+
171
+ When installing any approved or low-footprint tool:
172
+ 1. Download it into `~/.arisa/tools/<name>`. For the official catalog, clone shallowly and copy the subdirectory, for example:
173
+ `git clone --depth 1 https://github.com/clasen/Arisa /tmp/arisa-catalog && cp -R /tmp/arisa-catalog/tools/<name> ~/.arisa/tools/<name>`.
174
+ 2. Install dependencies inside the tool directory with `pnpm install`, falling back to `npm install`. Do not ask the user to install dependencies manually.
175
+ 3. Run it through `run_tool`; the registry picks up new tools automatically and exposes the Arisa package root through `ARISA_PACKAGE_DIR`. If it returns `missingConfig`, follow the missing config flow.
179
176
 
180
177
  When creating or editing tools:
181
- - always create tools under `~/.arisa/tools/<toolName>`, never inside the Arisa install directory
182
- - use the path helpers in `src/runtime/paths.js`
178
+ - follow the core boundary above: create or edit installed tools under `~/.arisa/tools/<toolName>`, never inside the Arisa install directory
179
+ - use the path helpers in `src/runtime/paths.js` for state, config, artifacts, and tmp paths
183
180
  - import Arisa core helpers dynamically through `ARISA_PACKAGE_DIR` (for example `await importCore("core/tools/tool-result.js")`); never use `../../src/...` or rewritten absolute paths
184
181
  - follow the tools in the official catalog as the reference pattern for new tools
185
182
  - keep all help text, usage instructions, manifests, and user-facing operational strings in English
@@ -198,13 +195,7 @@ Tools may declare skills in `tool.manifest.json`:
198
195
 
199
196
  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.
200
197
 
201
- ## Dependency installation
202
- Tool dependencies are installed as part of building or running the tool, not delegated to the user.
203
- - Prefer `pnpm install`.
204
- - Fall back to `npm install`.
205
- - Do not ask the user to do it manually.
206
-
207
198
  ## Safety
208
199
  - Prefer tool manifests and CLI help over assumptions.
209
- - Keep tool config and runtime data inside the user runtime area.
210
- - Be proactive about extending capabilities, but do it through the project's tool architecture, not through ad hoc one-off behavior.
200
+ - Keep config and runtime data inside the user runtime area through the path helpers above.
201
+ - Be proactive about extending capabilities through the tool architecture, not ad hoc one-off behavior.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "4.0.20",
3
+ "version": "4.0.24",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -58,8 +58,9 @@ export function createArisaClient({ toolName, chatId = null, socketPath = proces
58
58
  throw new Error("toolName is required");
59
59
  }
60
60
 
61
- const call = (method, params = {}) => requestIpc({
61
+ const call = (method, params = {}, options = {}) => requestIpc({
62
62
  socketPath,
63
+ timeoutMs: options.timeoutMs,
63
64
  request: {
64
65
  id: crypto.randomUUID(),
65
66
  method,
@@ -83,6 +84,9 @@ export function createArisaClient({ toolName, chatId = null, socketPath = proces
83
84
  agent: {
84
85
  enqueueEvent: (params) => call("agent.enqueueEvent", params)
85
86
  },
87
+ tools: {
88
+ run: (params, options) => call("tools.run", params, options)
89
+ },
86
90
  paths: {
87
91
  getChatToolStateDir: () => call("paths.getChatToolStateDir"),
88
92
  getToolStateDir: () => call("paths.getToolStateDir"),
@@ -27,16 +27,49 @@ function requireString(value, fieldName) {
27
27
  return value;
28
28
  }
29
29
 
30
+ function normalizeArgs(args) {
31
+ if (args == null) return {};
32
+ if (typeof args !== "object" || Array.isArray(args)) {
33
+ throw new Error("args must be an object");
34
+ }
35
+ return args;
36
+ }
37
+
30
38
  function normalizeLimit(limit) {
31
39
  const value = Number(limit);
32
40
  if (!Number.isInteger(value) || value <= 0) return 20;
33
41
  return Math.min(value, 100);
34
42
  }
35
43
 
36
- export function createArisaCapabilities({ artifactStore, taskStore } = {}) {
44
+ export function createArisaCapabilities({ artifactStore, taskStore, agentManager } = {}) {
37
45
  async function dispatch({ method, toolName, chatId = null, params = {} } = {}) {
38
46
  const scopedToolName = requireToolName(toolName);
39
47
 
48
+ if (method === "tools.run") {
49
+ if (!agentManager?.runTool) {
50
+ throw new Error("tools.run requires agentManager");
51
+ }
52
+ const scopedChatId = requireChatId(chatId, method);
53
+ const targetToolName = requireString(params.name, "name");
54
+ const chatArtifactStore = artifactStore.forChat(scopedChatId);
55
+ const artifact = params.artifactId
56
+ ? await chatArtifactStore.get(requireString(params.artifactId, "artifactId"))
57
+ : null;
58
+ if (params.artifactId && !artifact) {
59
+ throw new Error(`Artifact not found: ${params.artifactId}`);
60
+ }
61
+
62
+ return agentManager.runTool({
63
+ name: targetToolName,
64
+ request: {
65
+ artifact,
66
+ text: params.text,
67
+ args: normalizeArgs(params.args)
68
+ },
69
+ chatId: scopedChatId
70
+ });
71
+ }
72
+
40
73
  if (method === "artifacts.createText") {
41
74
  const scopedChatId = requireChatId(chatId, method);
42
75
  return artifactStore.forChat(scopedChatId).createText({
@@ -62,7 +62,7 @@ export async function createApp({ logger, runtimeOverrides } = {}) {
62
62
  logger?.log("app", `loaded ${toolRegistry.list().length} tools`);
63
63
 
64
64
  const agentManager = new AgentManager({ config, artifactStore, toolRegistry, taskStore, logger });
65
- const arisaCapabilities = createArisaCapabilities({ artifactStore, taskStore });
65
+ const arisaCapabilities = createArisaCapabilities({ artifactStore, taskStore, agentManager });
66
66
  const ipcServer = createIpcServer({ capabilities: arisaCapabilities, logger });
67
67
  const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger });
68
68
 
@@ -59,13 +59,18 @@ function createFakeTaskStore() {
59
59
  };
60
60
  }
61
61
 
62
- function createCapabilities() {
62
+ function createCapabilities(overrides = {}) {
63
63
  return createArisaCapabilities({
64
- artifactStore: createFakeArtifactStore(),
65
- taskStore: createFakeTaskStore()
64
+ artifactStore: overrides.artifactStore || createFakeArtifactStore(),
65
+ taskStore: overrides.taskStore || createFakeTaskStore(),
66
+ agentManager: overrides.agentManager
66
67
  });
67
68
  }
68
69
 
70
+ function wait(ms) {
71
+ return new Promise((resolve) => setTimeout(resolve, ms));
72
+ }
73
+
69
74
  async function createTempSocketPath() {
70
75
  const root = await mkdtemp(path.join(os.tmpdir(), "arisa-ipc-"));
71
76
  return path.join(root, "arisa.sock");
@@ -141,6 +146,114 @@ test("requires chatId for chat-scoped capabilities", async () => {
141
146
  }
142
147
  });
143
148
 
149
+ test("runs a registered tool over local IPC", async () => {
150
+ const socketPath = await createTempSocketPath();
151
+ const calls = [];
152
+ const agentManager = {
153
+ runTool: async (request) => {
154
+ calls.push(request);
155
+ return { ok: true, status: "ok", output: { text: "generated code" } };
156
+ }
157
+ };
158
+ const ipcServer = createIpcServer({ capabilities: createCapabilities({ agentManager }), socketPath });
159
+ await ipcServer.start();
160
+
161
+ try {
162
+ const client = createArisaClient({ toolName: "strudel-ui", chatId: "chat-1", socketPath });
163
+ const result = await client.tools.run({
164
+ name: "strudel-agent",
165
+ text: "make acid house",
166
+ args: {
167
+ bpm: 128,
168
+ tags: ["acid", "live"],
169
+ currentCode: "stack(s(\"bd\"))"
170
+ }
171
+ });
172
+
173
+ assert.deepEqual(result, { ok: true, status: "ok", output: { text: "generated code" } });
174
+ assert.equal(calls.length, 1);
175
+ assert.equal(calls[0].name, "strudel-agent");
176
+ assert.equal(calls[0].chatId, "chat-1");
177
+ assert.equal(calls[0].request.text, "make acid house");
178
+ assert.deepEqual(calls[0].request.args, {
179
+ bpm: 128,
180
+ tags: ["acid", "live"],
181
+ currentCode: "stack(s(\"bd\"))"
182
+ });
183
+ assert.equal(calls[0].request.artifact, null);
184
+ } finally {
185
+ await ipcServer.stop();
186
+ }
187
+ });
188
+
189
+ test("hydrates artifact input before running a tool over IPC", async () => {
190
+ const socketPath = await createTempSocketPath();
191
+ const calls = [];
192
+ const agentManager = {
193
+ runTool: async (request) => {
194
+ calls.push(request);
195
+ return { ok: true, status: "ok", output: { json: { received: true } } };
196
+ }
197
+ };
198
+ const ipcServer = createIpcServer({ capabilities: createCapabilities({ agentManager }), socketPath });
199
+ await ipcServer.start();
200
+
201
+ try {
202
+ const client = createArisaClient({ toolName: "strudel-ui", chatId: 123, socketPath });
203
+ const artifact = await client.artifacts.createText({ text: "current pattern" });
204
+ await client.tools.run({ name: "strudel-agent", artifactId: artifact.id });
205
+
206
+ assert.equal(calls.length, 1);
207
+ assert.deepEqual(calls[0].request.artifact, artifact);
208
+ } finally {
209
+ await ipcServer.stop();
210
+ }
211
+ });
212
+
213
+ test("requires chatId when running a tool over IPC", async () => {
214
+ const socketPath = await createTempSocketPath();
215
+ const agentManager = {
216
+ runTool: async () => ({ ok: true, status: "ok" })
217
+ };
218
+ const ipcServer = createIpcServer({ capabilities: createCapabilities({ agentManager }), socketPath });
219
+ await ipcServer.start();
220
+
221
+ try {
222
+ const client = createArisaClient({ toolName: "strudel-ui", socketPath });
223
+ await assert.rejects(
224
+ () => client.tools.run({ name: "strudel-agent" }),
225
+ /tools\.run requires chatId/
226
+ );
227
+ } finally {
228
+ await ipcServer.stop();
229
+ }
230
+ });
231
+
232
+ test("supports per-call IPC timeout overrides", async () => {
233
+ const socketPath = await createTempSocketPath();
234
+ const agentManager = {
235
+ runTool: async () => {
236
+ await wait(25);
237
+ return { ok: true, status: "ok" };
238
+ }
239
+ };
240
+ const ipcServer = createIpcServer({ capabilities: createCapabilities({ agentManager }), socketPath });
241
+ await ipcServer.start();
242
+
243
+ try {
244
+ const client = createArisaClient({ toolName: "strudel-ui", chatId: 123, socketPath });
245
+ const result = await client.tools.run({ name: "strudel-agent" }, { timeoutMs: 250 });
246
+ assert.deepEqual(result, { ok: true, status: "ok" });
247
+
248
+ await assert.rejects(
249
+ () => client.tools.run({ name: "strudel-agent" }, { timeoutMs: 1 }),
250
+ /Arisa IPC request timed out/
251
+ );
252
+ } finally {
253
+ await ipcServer.stop();
254
+ }
255
+ });
256
+
144
257
  test("listens on a local socket, not a TCP port", async () => {
145
258
  const socketPath = await createTempSocketPath();
146
259
  const ipcServer = createIpcServer({ capabilities: createCapabilities(), socketPath });