arisa 4.1.16 → 4.2.2
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 +13 -0
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +19 -3
- package/src/core/agent/runtime-context.js +13 -0
- package/src/core/artifacts/artifact-store.js +6 -1
- package/src/core/tools/tool-registry.js +39 -7
- package/test/agent-tool-policy.test.js +20 -0
- package/test/artifact-store.test.js +14 -0
- package/test/tool-registry-run.test.js +40 -3
package/AGENTS.md
CHANGED
|
@@ -23,6 +23,15 @@ Do not build runtime paths by hand. Use `src/runtime/paths.js`:
|
|
|
23
23
|
|
|
24
24
|
Tools receive `chatId` from the registry. Any persisted or indexed user content must be scoped by chat. Avoid ad hoc roots like `~/.arisa/state/<toolName>`, `~/.arisa/state/chats`, or runtime data inside `~/.arisa/tools/<toolName>`.
|
|
25
25
|
|
|
26
|
+
## Tool config rules
|
|
27
|
+
Every tool has a local `config.js` that exports generic, non-user-specific defaults. Keep user credentials, deployment-specific URLs, account ids, phone numbers, and other per-user values empty or neutral there; document required values in `tool.manifest.json` `configSchema`.
|
|
28
|
+
|
|
29
|
+
Tools must consume config through `src/core/tools/tool-config.js`:
|
|
30
|
+
- `loadToolConfig(toolName, defaults)` for global tools and daemons that run as one shared process.
|
|
31
|
+
- `loadToolConfig(toolName, defaults, request.chatId)` inside `run()` for request-scoped tools that should honor per-chat overrides.
|
|
32
|
+
|
|
33
|
+
Config precedence is a shallow merge: tool defaults -> global tool config (`~/.arisa/tools/<toolName>/config.js`) -> chat tool config (`~/.arisa/chats/<chatId>/config/tools/<toolName>/config.js`). Per-request `args` may override loaded config for that invocation only; do not persist request args as config unless the user explicitly asked to set a default.
|
|
34
|
+
|
|
26
35
|
## Main rule: everything is piped through artifacts
|
|
27
36
|
A pipe transforms one input artifact into one output artifact.
|
|
28
37
|
Examples:
|
|
@@ -34,6 +43,8 @@ Each tool declares in `tool.manifest.json`:
|
|
|
34
43
|
- `input`: supported input types
|
|
35
44
|
- `output`: produced output types
|
|
36
45
|
- `configSchema`: required config fields
|
|
46
|
+
- `category`: optional broad capability bucket for discovery
|
|
47
|
+
- `keywords`: optional intent tags for capability discovery
|
|
37
48
|
- `skillHints`: optional skills to apply when using or editing the tool
|
|
38
49
|
|
|
39
50
|
## Tool-to-Arisa IPC
|
|
@@ -157,6 +168,8 @@ Do not assume a rigid question/answer protocol. Continue the conversation natura
|
|
|
157
168
|
## Capability resolution
|
|
158
169
|
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.
|
|
159
170
|
|
|
171
|
+
Before asking the user for recurrent context, inspect available tools by `category` and `keywords`, especially `memory`, `context`, `contacts`, and `essential`.
|
|
172
|
+
|
|
160
173
|
When the user asks for something new:
|
|
161
174
|
1. check whether an existing registered tool, or an indirect use of one, can satisfy the task
|
|
162
175
|
2. check the official catalog at `https://github.com/clasen/Arisa/tree/main/tools` before building anything
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { stat, unlink } from "node:fs/promises";
|
|
3
|
-
import { createAgentSession, SessionManager, defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { readFile, stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { createAgentSession, DefaultResourceLoader, SessionManager, defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
|
|
6
|
-
import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
|
|
6
|
+
import { appendArisaAgentsFile, arisaAgentsFile, arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
|
|
7
7
|
import { withTimeout } from "./prompt-timeout.js";
|
|
8
8
|
import { buildPiToolPolicy, getCoreCodingTools } from "./core-tools.js";
|
|
9
9
|
import { createSystemShellTool } from "./system-shell-tool.js";
|
|
@@ -88,6 +88,17 @@ async function assertDirectory(dir, label) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
async function createArisaResourceLoader({ cwd, agentDir }) {
|
|
92
|
+
const arisaAgentsContent = await readFile(arisaAgentsFile, "utf8");
|
|
93
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
94
|
+
cwd,
|
|
95
|
+
agentDir,
|
|
96
|
+
agentsFilesOverride: (current) => appendArisaAgentsFile(current, arisaAgentsContent)
|
|
97
|
+
});
|
|
98
|
+
await resourceLoader.reload();
|
|
99
|
+
return resourceLoader;
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
export class AgentManager {
|
|
92
103
|
constructor({ config, artifactStore, toolRegistry, taskStore, logger }) {
|
|
93
104
|
this.config = config;
|
|
@@ -188,9 +199,14 @@ export class AgentManager {
|
|
|
188
199
|
...this.createTools(telegram, chatId, policy),
|
|
189
200
|
createSystemShellTool({ workspaceDir: policy.workspaceDir, shell: policy.shell })
|
|
190
201
|
];
|
|
202
|
+
const resourceLoader = await createArisaResourceLoader({
|
|
203
|
+
cwd: policy.workspaceDir,
|
|
204
|
+
agentDir: arisaHomeDir
|
|
205
|
+
});
|
|
191
206
|
const { session } = await createAgentSession({
|
|
192
207
|
cwd: policy.workspaceDir,
|
|
193
208
|
agentDir: arisaHomeDir,
|
|
209
|
+
resourceLoader,
|
|
194
210
|
authStorage,
|
|
195
211
|
modelRegistry,
|
|
196
212
|
model,
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { fileURLToPath } from "node:url";
|
|
2
3
|
import { arisaHomeDir, arisaPackageDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../runtime/paths.js";
|
|
3
4
|
|
|
4
5
|
export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
|
|
6
|
+
export const arisaAgentsFile = path.join(arisaPackageDir, "AGENTS.md");
|
|
7
|
+
|
|
8
|
+
export function appendArisaAgentsFile(current, content, agentsFilePath = arisaAgentsFile) {
|
|
9
|
+
return {
|
|
10
|
+
...current,
|
|
11
|
+
agentsFiles: [
|
|
12
|
+
...(current.agentsFiles || []).filter((file) => path.resolve(file.path) !== path.resolve(agentsFilePath)),
|
|
13
|
+
{ path: agentsFilePath, content }
|
|
14
|
+
]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
5
17
|
|
|
6
18
|
function formatCoreTools(coreTools = []) {
|
|
7
19
|
const enabled = coreTools
|
|
@@ -20,6 +32,7 @@ export function buildAgentRuntimeContext({ workspaceDir = arisaHomeDir, coreTool
|
|
|
20
32
|
`toolStateDir: ${toolStateDir}`,
|
|
21
33
|
`chatsDir: ${chatsDir}`,
|
|
22
34
|
`stateDir: ${stateDir}`,
|
|
35
|
+
`arisaAgentsFile: ${arisaAgentsFile}`,
|
|
23
36
|
`enabledCoreTools: ${formatCoreTools(coreTools)}`,
|
|
24
37
|
"Guidance: create or edit user tools under userToolsDir. Treat arisaPackageDir/arisaInstallDir as Arisa core and modify it only when the user explicitly asks for core changes."
|
|
25
38
|
].join("\n");
|
|
@@ -7,6 +7,11 @@ function id() {
|
|
|
7
7
|
return crypto.randomUUID();
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function writeArtifactFile(filePath, content) {
|
|
11
|
+
if (typeof content === "string") return writeFile(filePath, content, "utf8");
|
|
12
|
+
return writeFile(filePath, content);
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
class ChatArtifactStore {
|
|
11
16
|
constructor(chatId) {
|
|
12
17
|
this.chatId = String(chatId);
|
|
@@ -81,7 +86,7 @@ class ChatArtifactStore {
|
|
|
81
86
|
const dir = path.join(this.rootDir, artifactId);
|
|
82
87
|
await mkdir(dir, { recursive: true });
|
|
83
88
|
const destPath = path.join(dir, fileName);
|
|
84
|
-
await
|
|
89
|
+
await writeArtifactFile(destPath, content);
|
|
85
90
|
const artifact = {
|
|
86
91
|
id: artifactId,
|
|
87
92
|
chatId: this.chatId,
|
|
@@ -21,6 +21,28 @@ function runProcess(command, args, options = {}) {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function normalizeCategory(category) {
|
|
25
|
+
if (typeof category !== "string") return null;
|
|
26
|
+
const trimmed = category.trim();
|
|
27
|
+
return trimmed || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeKeywords(keywords) {
|
|
31
|
+
if (!Array.isArray(keywords)) return [];
|
|
32
|
+
return [...new Set(keywords
|
|
33
|
+
.filter((keyword) => typeof keyword === "string")
|
|
34
|
+
.map((keyword) => keyword.trim())
|
|
35
|
+
.filter(Boolean))];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatSemanticMetadata(tool) {
|
|
39
|
+
return [
|
|
40
|
+
"Semantic metadata:",
|
|
41
|
+
`- category: ${tool.category || "none"}`,
|
|
42
|
+
`- keywords: ${tool.keywords?.length ? tool.keywords.join(", ") : "none"}`
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
24
46
|
export class ToolRegistry {
|
|
25
47
|
constructor({ logger } = {}) {
|
|
26
48
|
this.logger = logger;
|
|
@@ -52,6 +74,8 @@ export class ToolRegistry {
|
|
|
52
74
|
const skillHints = this.skillRegistry.normalizeHints(manifest);
|
|
53
75
|
this.tools.set(manifest.name, {
|
|
54
76
|
...manifest,
|
|
77
|
+
category: normalizeCategory(manifest.category),
|
|
78
|
+
keywords: normalizeKeywords(manifest.keywords),
|
|
55
79
|
skillHints,
|
|
56
80
|
dir: toolDir,
|
|
57
81
|
entry: path.join(toolDir, manifest.entry || "index.js"),
|
|
@@ -75,6 +99,8 @@ export class ToolRegistry {
|
|
|
75
99
|
input: tool.input,
|
|
76
100
|
output: tool.output,
|
|
77
101
|
configSchema: tool.configSchema || {},
|
|
102
|
+
category: tool.category,
|
|
103
|
+
keywords: tool.keywords || [],
|
|
78
104
|
skillHints: tool.skillHints || []
|
|
79
105
|
}));
|
|
80
106
|
}
|
|
@@ -89,13 +115,19 @@ export class ToolRegistry {
|
|
|
89
115
|
const result = await runProcess("node", [tool.entry, "--help"], { cwd: tool.dir, env: toolEnv() });
|
|
90
116
|
const help = result.stdout || result.stderr;
|
|
91
117
|
const skills = await this.resolveSkills(name);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
const sections = [
|
|
119
|
+
help.trimEnd(),
|
|
120
|
+
formatSemanticMetadata(tool)
|
|
121
|
+
];
|
|
122
|
+
if (skills.length) {
|
|
123
|
+
const skillHelp = skills.map((item) => [
|
|
124
|
+
`- ${item.name}${item.when ? ` (${item.when})` : ""}`,
|
|
125
|
+
item.description ? ` ${item.description}` : null,
|
|
126
|
+
item.found ? ` path: ${item.path}` : " warning: skill not found"
|
|
127
|
+
].filter(Boolean).join("\n")).join("\n");
|
|
128
|
+
sections.push(`Assigned skills:\n${skillHelp}`);
|
|
129
|
+
}
|
|
130
|
+
return `${sections.filter(Boolean).join("\n\n")}\n`;
|
|
99
131
|
}
|
|
100
132
|
|
|
101
133
|
async resolveSkills(name) {
|
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import { buildPiToolPolicy } from "../src/core/agent/core-tools.js";
|
|
7
|
+
import { appendArisaAgentsFile, arisaAgentsFile } from "../src/core/agent/runtime-context.js";
|
|
7
8
|
import { createSystemShellTool } from "../src/core/agent/system-shell-tool.js";
|
|
8
9
|
import { applyRuntimeOverrides } from "../src/runtime/create-app.js";
|
|
9
10
|
import { arisaHomeDir } from "../src/runtime/paths.js";
|
|
@@ -89,3 +90,22 @@ test("system_shell runs commands from the configured workspace", async () => {
|
|
|
89
90
|
assert.equal(result.details.exitCode, 0);
|
|
90
91
|
assert.equal(result.details.stdout, realWorkspaceDir);
|
|
91
92
|
});
|
|
93
|
+
|
|
94
|
+
test("adds Arisa AGENTS.md to Pi context files without duplicating it", () => {
|
|
95
|
+
const current = {
|
|
96
|
+
agentsFiles: [
|
|
97
|
+
{ path: "/workspace/AGENTS.md", content: "# Workspace" },
|
|
98
|
+
{ path: arisaAgentsFile, content: "old" }
|
|
99
|
+
],
|
|
100
|
+
diagnostics: []
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const next = appendArisaAgentsFile(current, "# Arisa AGENTS");
|
|
104
|
+
|
|
105
|
+
assert.deepEqual(next.diagnostics, []);
|
|
106
|
+
assert.deepEqual(
|
|
107
|
+
next.agentsFiles.map((file) => file.path),
|
|
108
|
+
["/workspace/AGENTS.md", arisaAgentsFile]
|
|
109
|
+
);
|
|
110
|
+
assert.equal(next.agentsFiles.at(-1).content, "# Arisa AGENTS");
|
|
111
|
+
});
|
|
@@ -83,6 +83,20 @@ test("creates generated file artifacts", async () => {
|
|
|
83
83
|
assert.equal(artifact.mimeType, "text/markdown");
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
test("writes generated text file artifacts as UTF-8", async () => {
|
|
87
|
+
await resetHome();
|
|
88
|
+
const content = "# Español\nÑandú\n";
|
|
89
|
+
const artifact = await new ArtifactStore().forChat("chat-1").createGeneratedFile({
|
|
90
|
+
fileName: "reply.txt",
|
|
91
|
+
content,
|
|
92
|
+
kind: "document",
|
|
93
|
+
mimeType: "text/plain",
|
|
94
|
+
source: { type: "assistant" }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(await readFile(artifact.path), Buffer.from(content, "utf8"));
|
|
98
|
+
});
|
|
99
|
+
|
|
86
100
|
test("keeps artifact indexes isolated by chat", async () => {
|
|
87
101
|
await resetHome();
|
|
88
102
|
const store = new ArtifactStore();
|
|
@@ -20,10 +20,10 @@ async function resetHome() {
|
|
|
20
20
|
await rm(arisaHomeDir, { recursive: true, force: true });
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async function createFakeTool(name = "fake-tool") {
|
|
23
|
+
async function createFakeTool(name = "fake-tool", manifestOverrides = {}) {
|
|
24
24
|
const dir = path.join(toolsDir, name);
|
|
25
25
|
await mkdir(dir, { recursive: true });
|
|
26
|
-
|
|
26
|
+
const manifest = {
|
|
27
27
|
name,
|
|
28
28
|
description: "Fake test tool",
|
|
29
29
|
entry: "index.js",
|
|
@@ -33,6 +33,10 @@ async function createFakeTool(name = "fake-tool") {
|
|
|
33
33
|
apiKey: { type: "string", required: false }
|
|
34
34
|
},
|
|
35
35
|
skillHints: [{ name: "missing-skill", when: "testing" }]
|
|
36
|
+
};
|
|
37
|
+
await writeFile(path.join(dir, "tool.manifest.json"), `${JSON.stringify({
|
|
38
|
+
...manifest,
|
|
39
|
+
...manifestOverrides
|
|
36
40
|
}, null, 2)}\n`, "utf8");
|
|
37
41
|
await writeFile(path.join(dir, "config.js"), "export default {\n apiKey: \"default-key\"\n};\n", "utf8");
|
|
38
42
|
await writeFile(path.join(dir, "index.js"), `import { readFile } from "node:fs/promises";
|
|
@@ -67,7 +71,10 @@ process.stdout.write(JSON.stringify({
|
|
|
67
71
|
|
|
68
72
|
test("loads and lists installed tools from the user tools directory", async () => {
|
|
69
73
|
await resetHome();
|
|
70
|
-
await createFakeTool("fake-tool"
|
|
74
|
+
await createFakeTool("fake-tool", {
|
|
75
|
+
category: "memory",
|
|
76
|
+
keywords: ["memory", "essential"]
|
|
77
|
+
});
|
|
71
78
|
|
|
72
79
|
const registry = new ToolRegistry();
|
|
73
80
|
await registry.load();
|
|
@@ -80,10 +87,40 @@ test("loads and lists installed tools from the user tools directory", async () =
|
|
|
80
87
|
configSchema: {
|
|
81
88
|
apiKey: { type: "string", required: false }
|
|
82
89
|
},
|
|
90
|
+
category: "memory",
|
|
91
|
+
keywords: ["memory", "essential"],
|
|
83
92
|
skillHints: [{ name: "missing-skill", when: "testing" }]
|
|
84
93
|
}]);
|
|
85
94
|
});
|
|
86
95
|
|
|
96
|
+
test("lists optional semantic metadata with stable defaults", async () => {
|
|
97
|
+
await resetHome();
|
|
98
|
+
await createFakeTool("fake-tool");
|
|
99
|
+
|
|
100
|
+
const registry = new ToolRegistry();
|
|
101
|
+
await registry.load();
|
|
102
|
+
|
|
103
|
+
assert.equal(registry.list()[0].category, null);
|
|
104
|
+
assert.deepEqual(registry.list()[0].keywords, []);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("shows semantic metadata in tool help", async () => {
|
|
108
|
+
await resetHome();
|
|
109
|
+
await createFakeTool("fake-tool", {
|
|
110
|
+
category: "memory",
|
|
111
|
+
keywords: ["memory", "essential"]
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const registry = new ToolRegistry();
|
|
115
|
+
await registry.load();
|
|
116
|
+
|
|
117
|
+
const help = await registry.help("fake-tool");
|
|
118
|
+
|
|
119
|
+
assert.match(help, /Fake test tool help/);
|
|
120
|
+
assert.match(help, /Semantic metadata:\n- category: memory\n- keywords: memory, essential/);
|
|
121
|
+
assert.match(help, /Assigned skills:/);
|
|
122
|
+
});
|
|
123
|
+
|
|
87
124
|
test("runs a registered tool process with an enriched request and cleans up request files", async () => {
|
|
88
125
|
await resetHome();
|
|
89
126
|
await createFakeTool("fake-tool");
|