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 +18 -2
- package/package.json +1 -1
- package/src/core/tools/ipc-client.js +5 -1
- package/src/runtime/arisa-capabilities.js +34 -1
- package/src/runtime/create-app.js +1 -1
- package/test/ipc-server.test.js +116 -3
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
|
-
|
|
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
|
@@ -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
|
|
package/test/ipc-server.test.js
CHANGED
|
@@ -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 });
|