arisa 4.0.20 → 4.0.22

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
@@ -8,6 +8,12 @@
8
8
  - Tools are isolated and each one has its own manifest, entrypoint, and config defaults.
9
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.
10
10
 
11
+ ## Core modification policy
12
+ Modifying the Arisa core is the last resort, never the default. All work must be done in tools.
13
+ - Always solve the request by creating or editing a tool under `~/.arisa/tools/<toolName>`.
14
+ - Treat changing core code as the final option, only after confirming the capability genuinely cannot be delivered through the tool architecture.
15
+ - Never modify the core on your own initiative. Always consult the user first, explaining why a core change is unavoidable, and wait for explicit approval before touching core code.
16
+
11
17
  ## Runtime directory rules
12
18
  Do not build runtime paths by hand. Use `src/runtime/paths.js`:
13
19
  - `getToolDir(toolName)`: installed user tool package only; no runtime data here.
@@ -33,7 +39,7 @@ Each tool declares in `tool.manifest.json`:
33
39
  - `skillHints`: optional skills to apply when using or editing the tool
34
40
 
35
41
  ## 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.
42
+ 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 to run a registered tool, create/read artifacts, manage tasks, enqueue agent events, or resolve runtime paths.
37
43
 
38
44
  Installed tools can import the IPC client through `ARISA_PACKAGE_DIR`:
39
45
 
@@ -48,7 +54,17 @@ const arisa = createArisaClient({ toolName: "example-tool", chatId });
48
54
  await arisa.artifacts.createText({ text: "hello" });
49
55
  ```
50
56
 
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`.
57
+ For tool-owned web UI request/response flows, call another registered tool through IPC instead of adding core HTTP routes or proxies:
58
+
59
+ ```js
60
+ const result = await arisa.tools.run({
61
+ name: "strudel-agent",
62
+ text: prompt,
63
+ args: { bpm, tags, currentCode }
64
+ }, { timeoutMs: 120_000 });
65
+ ```
66
+
67
+ 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 add raw access to `agentManager`, `taskStore`, `artifactStore`, or `toolRegistry`.
52
68
 
53
69
  ## Conceptual pipe model
54
70
  There are two different moments where pipes can happen:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "4.0.20",
3
+ "version": "4.0.22",
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 });