arisa 3.0.14 → 3.1.4

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
@@ -3,10 +3,21 @@
3
3
  ## Architecture
4
4
  - Telegram transport handles inbound and outbound messaging.
5
5
  - Pi Agent keeps one session per authorized chat.
6
- - Every incoming or generated message or file becomes an artifact.
6
+ - Incoming messages and files (text, voice, photo, document) and generated files become artifacts.
7
7
  - A tool registry handles tool discovery, help lookup, config writes, and execution.
8
8
  - Tools are isolated and each one has its own manifest, entrypoint, and config defaults.
9
9
 
10
+ ## Runtime directory rules
11
+ Do not build runtime paths by hand. Use `src/runtime/paths.js`:
12
+ - `getToolDir(toolName)`: installed user tool package only; no runtime data here.
13
+ - `getToolStateDir(toolName)`: global tool infrastructure only: daemons, queues, shared browser sessions, model caches.
14
+ - `getChatToolStateDir(chatId, toolName)`: persistent user/chat data: tool DBs, indexes, inboxes, generated sites, vaults.
15
+ - `getChatArtifactsDir(chatId)` / `getChatArtifactsIndexFile(chatId)`: chat artifacts and artifact index. Artifacts are never global.
16
+ - `getChatToolConfigPath(chatId, toolName)`: chat-scoped config overrides.
17
+ - `getToolTmpDir(toolName)` / `getChatToolTmpDir(chatId, toolName)`: ephemeral scratch. Create only while a request runs; remove when empty.
18
+
19
+ 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>`.
20
+
10
21
  ## Main rule: everything is piped through artifacts
11
22
  A pipe transforms one input artifact into one output artifact.
12
23
  Examples:
@@ -18,6 +29,7 @@ Each tool declares in `tool.manifest.json`:
18
29
  - `input`: supported input types
19
30
  - `output`: produced output types
20
31
  - `configSchema`: required config fields
32
+ - `skillHints`: optional skills to apply when using or editing the tool
21
33
 
22
34
  ## Conceptual pipe model
23
35
  There are two different moments where pipes can happen:
@@ -34,12 +46,11 @@ There are two different moments where pipes can happen:
34
46
  - Pi Agent may decide to chain tools to achieve a user goal.
35
47
  - Example: text -> TTS audio, or future multi-step workflows.
36
48
 
37
- This distinction is critical. Not every pipe should be decided by Pi Agent at runtime. Some pipes are part of the transport/input normalization layer and must happen before reasoning.
49
+ Not every pipe should be decided by Pi Agent at runtime. Some pipes are part of the transport/input normalization layer and must happen before reasoning.
38
50
 
39
51
  ## Telegram inbound pipeline
40
- Current conceptual behavior:
41
52
  - text -> send directly to Pi Agent
42
- - audio/voice -> transcribe first -> send transcript to Pi Agent
53
+ - voice -> transcribe first -> send transcript to Pi Agent
43
54
  - image/document/other media -> keep as artifacts, and add normalization pipes when needed
44
55
 
45
56
  If inbound media was normalized before reasoning, Pi Agent should use the normalized result as the actual message content.
@@ -50,12 +61,23 @@ Before using a tool, inspect its help:
50
61
  - via the custom tool: `tool_help`
51
62
  - or by running the CLI with `--help`
52
63
 
53
- Every CLI must support:
64
+ Every CLI must support (the entrypoint comes from `manifest.entry`, currently always `index.js`):
54
65
  - `node index.js --help`
55
66
  - `node index.js run --request-file <json>`
56
67
 
57
- ## Pipe behavior in V1
58
- V1 does not have a full automatic planner yet. The agent should:
68
+ ### Tools that need daemons
69
+ A future tool may need a persistent process, for example to keep a browser session alive or a local model warm. The shared daemon runtime exists for this, but no bundled tool uses it yet.
70
+ When such a tool is built, implement it with the shared daemon runtime instead of custom ad hoc process management:
71
+ - use `src/core/tools/daemon-runtime.js`
72
+ - keep runtime files under the tool state directory (`~/.arisa/state/tools/<toolName>`)
73
+ - expose normal CLI behavior through `run --request-file`; callers should not manage daemon internals
74
+ - use the runtime for `daemon.pid`, `daemon.log`, `status.json`, and `commands/*.request|processing|result.json`
75
+ - keep one daemon owner per tool/session and avoid opening a second client over the same resource
76
+ - use `beforeStart` only for tool-specific cleanup such as stale browser locks, without deleting persistent session/model data
77
+ - keep daemon tools headless/server-safe by default when they are meant to run on VPS machines
78
+
79
+ ## Manual pipe behavior
80
+ To run a pipe, the agent should:
59
81
  1. understand whether the needed pipe belongs to pre-reasoning normalization or post-reasoning tool chaining
60
82
  2. use `list_tools`
61
83
  3. use `tool_help` when it needs operational details
@@ -65,7 +87,28 @@ V1 does not have a full automatic planner yet. The agent should:
65
87
  Example manual pipe:
66
88
  1. `run_tool(openai-transcribe, artifact audio)`
67
89
  2. take the returned text `artifactId`
68
- 3. `run_tool(openai-tts, artifact text)` or `send_audio_reply(text)`
90
+ 3. `run_tool(openai-tts, artifact text)` or `send_media_reply(text)`
91
+
92
+ ## Async event queue flow
93
+ 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`:
94
+
95
+ - `agent_task`: a scheduled prompt. The poller delivers it as a prompt for Pi to fulfill (time-based work).
96
+ - `poll_tool`: a recurring checker the poller **runs directly as a tool** (no agent turn spent). The poller materializes its output with the same logic as `run_tool`, so any `agent_event` the checker emits is enqueued for the next tick. Its `recurrence` reschedules the next poll.
97
+ - `agent_event`: an incoming event. The poller delivers it as a prompt so Pi evaluates it and decides the next action (it may stay silent).
98
+
99
+ Tasks without a `runAt` fire immediately, so `agent_event` and the first `poll_tool` run on the next tick.
100
+
101
+ The poller dispatches all three kinds, but only `agent_task` is exercised by a bundled tool today (`schedule-agent-task`). The following is the pattern to follow when a checker tool is built:
102
+
103
+ How a tool wires its own polling:
104
+ 1. From any tool `run`, start the poll by returning an `asyncTask` (or several in `asyncTasks`):
105
+ `{ kind: "poll_tool", payload: { toolName, args }, recurrence: { type: "interval", everySeconds: N } }`.
106
+ 2. On each poll the checker tool (`toolName`) runs headless. It keeps its own cursor of seen state in its config/tmp per chat, so it knows what is new.
107
+ 3. When the checker finds something new, it emits an event from its `run`:
108
+ `{ kind: "agent_event", payload: { prompt: "<content to evaluate>" } }`.
109
+ 4. The agent reasons over the `agent_event` and decides what to do.
110
+
111
+ `list_scheduled_tasks`, `cancel_scheduled_task`, and `cancel_all_scheduled_tasks` are kind-agnostic, so they already work to inspect or cancel active polls.
69
112
 
70
113
  ## Missing config flow
71
114
  If `run_tool` returns `missingConfig`, the agent should:
@@ -90,13 +133,26 @@ The default attitude is:
90
133
  - propose or start creating the needed tool
91
134
 
92
135
  When creating or editing tools:
93
- - use the shared path helpers and the runtime paths provided in the prompt instead of assuming fixed locations
94
- - consult the local skill for that workflow when building new tools
136
+ - use the path helpers in `src/runtime/paths.js`
137
+ - follow the existing bundled tools under `tools/` as the reference pattern for new tools
95
138
  - keep all help text, usage instructions, manifests, and user-facing operational strings in English
96
139
  - follow the One Thing Rule: each function or method should do one thing well; if it mixes low-level operations with high-level policy, split it into smaller focused units
97
140
 
141
+ ### Tool skill hints
142
+ Tools may declare skills in `tool.manifest.json`:
143
+
144
+ ```json
145
+ {
146
+ "skillHints": [
147
+ { "name": "stop-slop", "when": "writing public page copy" }
148
+ ]
149
+ }
150
+ ```
151
+
152
+ 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.
153
+
98
154
  ## Dependency installation
99
- Arisa installs tool dependencies itself.
155
+ Tool dependencies are installed as part of building or running the tool, not delegated to the user.
100
156
  - Prefer `pnpm install`.
101
157
  - Fall back to `npm install`.
102
158
  - Do not ask the user to do it manually.
package/README.md CHANGED
@@ -101,6 +101,16 @@ arisa install <source> # install a Pi package into Arisa's runtime
101
101
  arisa remove <source> # remove a Pi package from Arisa's runtime
102
102
  ```
103
103
 
104
+ Runtime model override (current process only):
105
+
106
+ ```bash
107
+ arisa --pi.model lmstudio/google/gemma-4-26b-a4b
108
+ ```
109
+
110
+ Notes:
111
+
112
+ - it only affects the current Arisa process and does not update `~/.arisa/state/config.json`
113
+
104
114
  ## Experimental features
105
115
 
106
116
  ### Pi Agent packages
@@ -124,6 +134,33 @@ On first run, Arisa will:
124
134
  6. validate that Pi Agent works
125
135
  7. only then start listening to Telegram
126
136
 
137
+ ### Non-interactive bootstrap (CLI overrides)
138
+
139
+ You can skip the interactive questions by providing `--telegram.token` and optional overrides:
140
+
141
+ ```bash
142
+ node src/index.js --telegram.token <token>
143
+ ```
144
+
145
+ With this mode, Arisa creates `~/.arisa/state/config.json` without prompts and applies these defaults when not provided:
146
+
147
+ - `pi.provider`: `openai-codex` when available, otherwise first provider from the current Pi provider list
148
+ - `pi.model`: first model after bootstrap sorting (currently prioritizes `openai-codex/gpt-5.5`)
149
+ - `telegram.maxChatIds`: `1`
150
+
151
+ Supported overrides:
152
+
153
+ ```bash
154
+ node src/index.js --telegram.token <token> --telegram.maxChatIds 3 --pi.provider openai-codex --pi.model gpt-5.5 --pi.apiKey <optional-provider-key>
155
+ ```
156
+
157
+ Notes:
158
+
159
+ - interactive bootstrap remains unchanged when no CLI overrides are provided
160
+ - `--bootstrap` can be combined with overrides to regenerate config non-interactively
161
+ - when `--pi.apiKey` is omitted and the provider supports OAuth, Arisa starts a temporary web page on `PORT` (default `10000`) where you can complete authentication from any browser
162
+ - unknown `--pi.provider` or `--pi.model` values are ignored and replaced by safe defaults
163
+
127
164
  Telegram bot tokens can be created with:
128
165
 
129
166
  - https://t.me/BotFather
@@ -134,7 +171,7 @@ For providers with internal Pi login support, such as Codex, leaving the API key
134
171
 
135
172
  For example, selecting:
136
173
 
137
- - `openai-codex/gpt-5.4`
174
+ - `openai-codex/gpt-5.5`
138
175
 
139
176
  allows Arisa to authenticate through Pi's Codex OAuth flow instead of requiring a normal OpenAI API key.
140
177
 
package/package.json CHANGED
@@ -1,16 +1,12 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "3.0.14",
3
+ "version": "3.1.4",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
8
8
  "arisa": "bin/arisa.js"
9
9
  },
10
- "scripts": {
11
- "start": "node src/index.js",
12
- "bootstrap": "node src/index.js --bootstrap"
13
- },
14
10
  "keywords": [
15
11
  "telegram",
16
12
  "pi-agent",
@@ -32,10 +28,13 @@
32
28
  ],
33
29
  "author": "Martin Clasen",
34
30
  "license": "MIT",
35
- "packageManager": "pnpm@10.32.1",
36
31
  "dependencies": {
37
- "@mariozechner/pi-coding-agent": "^0.65.0",
32
+ "@mariozechner/pi-coding-agent": "^0.73.1",
38
33
  "@sinclair/typebox": "^0.34.41",
39
34
  "grammy": "^1.42.0"
35
+ },
36
+ "scripts": {
37
+ "start": "node src/index.js",
38
+ "bootstrap": "node src/index.js --bootstrap"
40
39
  }
41
- }
40
+ }
@@ -0,0 +1,12 @@
1
+ overrides:
2
+ '@protobufjs/utf8@<=1.1.0': '>=1.1.1'
3
+ basic-ftp@<=5.2.1: '>=5.2.2'
4
+ basic-ftp@<=5.2.2: '>=5.3.0'
5
+ basic-ftp@<=5.3.0: '>=5.3.1'
6
+ basic-ftp@=5.2.0: '>=5.2.1'
7
+ brace-expansion@>=5.0.0 <5.0.6: '>=5.0.6'
8
+ ip-address@<=10.1.0: '>=10.1.1'
9
+ protobufjs@<7.5.5: '>=7.5.5'
10
+ protobufjs@<=7.5.5: '>=7.5.6'
11
+ protobufjs@<=7.5.7: '>=7.5.8'
12
+ ws@>=8.0.0 <8.20.1: '>=8.20.1'
@@ -5,7 +5,21 @@ import { Type } from "@sinclair/typebox";
5
5
  import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
6
6
  import { loadProjectInstructions } from "./project-instructions.js";
7
7
  import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
8
- import { arisaHomeDir } from "../../runtime/paths.js";
8
+ import { arisaHomeDir, getChatPiSessionsDir } from "../../runtime/paths.js";
9
+
10
+ function isLocalBaseUrl(value) {
11
+ if (typeof value !== "string" || !value.trim()) return false;
12
+ try {
13
+ const parsed = new URL(value);
14
+ return parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost";
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ function requiresProviderAuth(model) {
21
+ return !isLocalBaseUrl(model?.baseUrl);
22
+ }
9
23
 
10
24
  export class AgentManager {
11
25
  constructor({ config, artifactStore, toolRegistry, taskStore, logger }) {
@@ -15,15 +29,30 @@ export class AgentManager {
15
29
  this.taskStore = taskStore;
16
30
  this.logger = logger;
17
31
  this.sessions = new Map();
32
+ this.pendingNewSessions = new Set();
18
33
  }
19
34
 
20
35
  setConfig(config) {
21
36
  this.config = config;
22
37
  this.sessions.clear();
38
+ this.pendingNewSessions.clear();
23
39
  }
24
40
 
25
41
  resetSession(chatId) {
26
- this.sessions.delete(chatId);
42
+ const sessionKey = String(chatId);
43
+ this.sessions.delete(sessionKey);
44
+ this.pendingNewSessions.add(sessionKey);
45
+ }
46
+
47
+ createSessionManager(chatId) {
48
+ const sessionKey = String(chatId);
49
+ const sessionDir = getChatPiSessionsDir(sessionKey);
50
+ if (this.pendingNewSessions.has(sessionKey)) {
51
+ this.logger?.log("agent", `starting new persisted session for chat ${sessionKey}`);
52
+ return { sessionManager: SessionManager.create(arisaInstallDir, sessionDir), isNewSession: true };
53
+ }
54
+ this.logger?.log("agent", `recovering persisted session for chat ${sessionKey}`);
55
+ return { sessionManager: SessionManager.continueRecent(arisaInstallDir, sessionDir), isNewSession: false };
27
56
  }
28
57
 
29
58
  async validatePiAgent() {
@@ -36,7 +65,7 @@ export class AgentManager {
36
65
  if (!model) {
37
66
  throw new Error(`Model not found: ${this.config.pi.provider}/${this.config.pi.model}`);
38
67
  }
39
- if (!this.config.pi.apiKey && !hasProviderAuth(this.config.pi.provider, { authStorage, modelRegistry })) {
68
+ if (requiresProviderAuth(model) && !this.config.pi.apiKey && !hasProviderAuth(this.config.pi.provider, { authStorage, modelRegistry })) {
40
69
  throw new Error(`No auth found for ${this.config.pi.provider}. Provide a Pi API key in bootstrap, or authenticate with Pi login for this provider during bootstrap.`);
41
70
  }
42
71
 
@@ -50,22 +79,32 @@ export class AgentManager {
50
79
  }
51
80
 
52
81
  async getSessionContext(chatId, telegram) {
53
- if (this.sessions.has(chatId)) {
54
- this.logger?.log("agent", `reusing session for chat ${chatId}`);
55
- return this.sessions.get(chatId);
82
+ const sessionKey = String(chatId);
83
+ const effectiveModelId = this.config.pi.model;
84
+ if (this.sessions.has(sessionKey)) {
85
+ const existing = this.sessions.get(sessionKey);
86
+ if (existing?.modelId === effectiveModelId) {
87
+ this.logger?.log("agent", `reusing session for chat ${sessionKey}`);
88
+ return existing;
89
+ }
90
+ this.logger?.log("agent", `model changed for chat ${sessionKey}: ${existing?.modelId || "unknown"} -> ${effectiveModelId}; recreating session`);
91
+ this.sessions.delete(sessionKey);
92
+ this.pendingNewSessions.add(sessionKey);
56
93
  }
57
94
 
58
95
  const { authStorage, modelRegistry } = createPiRuntime({
59
96
  provider: this.config.pi.provider,
60
97
  apiKey: this.config.pi.apiKey
61
98
  });
62
- const model = modelRegistry.find(this.config.pi.provider, this.config.pi.model);
63
- if (!model) throw new Error(`Model not found: ${this.config.pi.provider}/${this.config.pi.model}`);
64
- if (!this.config.pi.apiKey && !hasProviderAuth(this.config.pi.provider, { authStorage, modelRegistry })) {
99
+ const model = modelRegistry.find(this.config.pi.provider, effectiveModelId);
100
+ if (!model) throw new Error(`Model not found: ${this.config.pi.provider}/${effectiveModelId}`);
101
+ if (requiresProviderAuth(model) && !this.config.pi.apiKey && !hasProviderAuth(this.config.pi.provider, { authStorage, modelRegistry })) {
65
102
  throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
66
103
  }
67
104
 
68
- this.logger?.log("agent", `creating session for chat ${chatId}`);
105
+ const { sessionManager, isNewSession } = this.createSessionManager(sessionKey);
106
+ const hasExistingSession = sessionManager.buildSessionContext().messages.length > 0;
107
+ this.logger?.log("agent", `${hasExistingSession ? "resuming" : "creating"} session for chat ${sessionKey} with model ${effectiveModelId}`);
69
108
  const customTools = this.createTools(telegram, chatId);
70
109
  const { session } = await createAgentSession({
71
110
  cwd: arisaInstallDir,
@@ -74,21 +113,71 @@ export class AgentManager {
74
113
  modelRegistry,
75
114
  model,
76
115
  customTools,
77
- sessionManager: SessionManager.inMemory()
116
+ sessionManager
78
117
  });
79
118
 
80
- const instructions = await loadProjectInstructions();
81
- const runtimeContext = buildAgentRuntimeContext();
82
- this.logger?.log("agent", `injecting project instructions for chat ${chatId}`);
83
- this.logger?.log("agent", `runtime context for chat ${chatId}:\n${runtimeContext}`);
84
- await session.prompt(`${instructions}\n\n${runtimeContext}\n\nAcknowledge with exactly: OK`);
119
+ if (!hasExistingSession) {
120
+ const instructions = await loadProjectInstructions();
121
+ const runtimeContext = buildAgentRuntimeContext();
122
+ this.logger?.log("agent", `injecting project instructions for chat ${sessionKey}`);
123
+ this.logger?.log("agent", `runtime context for chat ${sessionKey}:\n${runtimeContext}`);
124
+ await session.prompt(`${instructions}\n\n${runtimeContext}\n\nAcknowledge with exactly: OK`);
125
+ }
85
126
 
86
- const ctx = { session };
87
- this.sessions.set(chatId, ctx);
127
+ const ctx = { session, modelId: effectiveModelId };
128
+ this.sessions.set(sessionKey, ctx);
129
+ if (isNewSession) {
130
+ this.pendingNewSessions.delete(sessionKey);
131
+ }
88
132
  return ctx;
89
133
  }
90
134
 
135
+ async runTool({ name, request, chatId }) {
136
+ await this.toolRegistry.load();
137
+ this.logger?.log("agent", `run_tool ${name}`);
138
+ const chatArtifactStore = this.artifactStore.forChat(chatId);
139
+ const result = await this.toolRegistry.run({ name, request, chatId });
140
+
141
+ if (result.output?.text) {
142
+ const outArtifact = await chatArtifactStore.createText({
143
+ text: result.output.text,
144
+ source: { type: "tool", toolName: name },
145
+ metadata: { tool: name }
146
+ });
147
+ result.output.artifactId = outArtifact.id;
148
+ }
149
+
150
+ if (result.output?.filePath) {
151
+ const generated = await chatArtifactStore.createFromFile({
152
+ originalPath: result.output.filePath,
153
+ fileName: result.output.fileName || path.basename(result.output.filePath),
154
+ kind: result.output.kind || "file",
155
+ mimeType: result.output.mimeType || "application/octet-stream",
156
+ source: { type: "tool", toolName: name },
157
+ metadata: { tool: name }
158
+ });
159
+ result.output.artifactId = generated.id;
160
+ await unlink(result.output.filePath).catch(() => {});
161
+ }
162
+
163
+ if (result.asyncTask || result.asyncTasks?.length) {
164
+ const scheduled = await this.taskStore.addMany(
165
+ result.asyncTasks || [result.asyncTask],
166
+ {
167
+ payload: { chatId },
168
+ source: { type: "tool", toolName: name, chatId }
169
+ }
170
+ );
171
+ result.asyncTasks = scheduled;
172
+ delete result.asyncTask;
173
+ }
174
+
175
+ return result;
176
+ }
177
+
91
178
  createTools(telegram, chatId) {
179
+ const chatArtifactStore = this.artifactStore.forChat(chatId);
180
+
92
181
  return [
93
182
  defineTool({
94
183
  name: "list_tools",
@@ -114,14 +203,26 @@ export class AgentManager {
114
203
  return { content: [{ type: "text", text: help }], details: { help } };
115
204
  }
116
205
  }),
206
+ defineTool({
207
+ name: "tool_skills",
208
+ label: "Tool skills",
209
+ description: "Show skills assigned to a CLI tool via its manifest skillHints.",
210
+ parameters: Type.Object({ name: Type.String() }),
211
+ execute: async (_id, params) => {
212
+ await this.toolRegistry.load();
213
+ const skills = await this.toolRegistry.resolveSkills(params.name);
214
+ const visible = skills.map(({ content, ...item }) => item);
215
+ return { content: [{ type: "text", text: JSON.stringify(visible, null, 2) }], details: visible };
216
+ }
217
+ }),
117
218
  defineTool({
118
219
  name: "set_tool_config",
119
220
  label: "Set tool config",
120
- description: "Write a value into ~/.arisa/tools/<tool>/config.js.",
221
+ description: "Write a tool config value scoped to the current chat.",
121
222
  parameters: Type.Object({ name: Type.String(), field: Type.String(), value: Type.String() }),
122
223
  execute: async (_id, params) => {
123
224
  await this.toolRegistry.load();
124
- const result = await this.toolRegistry.setConfig(params.name, params.field, params.value);
225
+ const result = await this.toolRegistry.setConfig(params.name, params.field, params.value, chatId);
125
226
  return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
126
227
  }
127
228
  }),
@@ -136,58 +237,23 @@ export class AgentManager {
136
237
  args: Type.Optional(Type.Record(Type.String(), Type.String()))
137
238
  }),
138
239
  execute: async (_id, params) => {
139
- await this.toolRegistry.load();
140
- this.logger?.log("agent", `run_tool ${params.name}`);
141
240
  let artifact = null;
142
241
  if (params.artifactId) {
143
- artifact = await this.artifactStore.get(params.artifactId);
242
+ artifact = await chatArtifactStore.get(params.artifactId);
144
243
  if (!artifact) {
145
244
  return { content: [{ type: "text", text: `Artifact not found: ${params.artifactId}` }], details: { ok: false } };
146
245
  }
147
246
  }
148
- const result = await this.toolRegistry.run({
247
+ const result = await this.runTool({
149
248
  name: params.name,
150
249
  request: {
151
250
  artifact,
152
251
  text: params.text,
153
252
  args: params.args || {}
154
- }
253
+ },
254
+ chatId
155
255
  });
156
256
 
157
- if (result.output?.text) {
158
- const outArtifact = await this.artifactStore.createText({
159
- text: result.output.text,
160
- source: { type: "tool", toolName: params.name },
161
- metadata: { tool: params.name }
162
- });
163
- result.output.artifactId = outArtifact.id;
164
- }
165
-
166
- if (result.output?.filePath) {
167
- const generated = await this.artifactStore.createFromFile({
168
- originalPath: result.output.filePath,
169
- fileName: result.output.fileName || path.basename(result.output.filePath),
170
- kind: result.output.kind || "file",
171
- mimeType: result.output.mimeType || "application/octet-stream",
172
- source: { type: "tool", toolName: params.name },
173
- metadata: { tool: params.name }
174
- });
175
- result.output.artifactId = generated.id;
176
- await unlink(result.output.filePath).catch(() => {});
177
- }
178
-
179
- if (result.asyncTask || result.asyncTasks?.length) {
180
- const scheduled = await this.taskStore.addMany(
181
- result.asyncTasks || [result.asyncTask],
182
- {
183
- payload: { chatId },
184
- source: { type: "tool", toolName: params.name, chatId }
185
- }
186
- );
187
- result.asyncTasks = scheduled;
188
- delete result.asyncTask;
189
- }
190
-
191
257
  return {
192
258
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
193
259
  details: result
@@ -261,7 +327,8 @@ export class AgentManager {
261
327
  this.logger?.log("agent", `send_media_reply via ${toolName}`);
262
328
  const result = await this.toolRegistry.run({
263
329
  name: toolName,
264
- request: { text: params.text, args: {} }
330
+ request: { text: params.text, args: {} },
331
+ chatId
265
332
  });
266
333
  if (!result.ok || !result.output?.filePath) {
267
334
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
@@ -1,5 +1,5 @@
1
1
  import { fileURLToPath } from "node:url";
2
- import { arisaHomeDir, artifactsDir, stateDir, toolsDir } from "../../runtime/paths.js";
2
+ import { arisaHomeDir, chatsDir, stateDir, toolStateDir, toolsDir } from "../../runtime/paths.js";
3
3
 
4
4
  export const arisaInstallDir = fileURLToPath(new URL("../../..", import.meta.url));
5
5
  export const bundledToolsDir = fileURLToPath(new URL("../../../tools", import.meta.url));
@@ -10,7 +10,8 @@ export function buildAgentRuntimeContext() {
10
10
  `arisaInstallDir: ${arisaInstallDir}`,
11
11
  `bundledToolsDir: ${bundledToolsDir}`,
12
12
  `userToolsDir: ${toolsDir}`,
13
- `artifactsDir: ${artifactsDir}`,
13
+ `toolStateDir: ${toolStateDir}`,
14
+ `chatsDir: ${chatsDir}`,
14
15
  `stateDir: ${stateDir}`
15
16
  ].join("\n");
16
17
  }