arisa 3.0.12 → 3.1.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 +11 -0
- package/README.md +37 -0
- package/docs/async-event-queue-flow.md +68 -0
- package/package.json +9 -10
- package/pnpm-workspace.yaml +12 -0
- package/src/core/agent/agent-manager.js +137 -28
- package/src/core/agent/runtime-context.js +2 -2
- package/src/core/artifacts/artifact-store.js +50 -25
- package/src/core/artifacts/normalize-for-reasoning.js +90 -0
- package/src/core/tasks/task-store.js +169 -0
- package/src/core/tools/daemon-runtime.js +167 -0
- package/src/core/tools/tool-config.js +15 -7
- package/src/core/tools/tool-registry.js +25 -10
- package/src/index.js +105 -12
- package/src/runtime/bootstrap.js +211 -19
- package/src/runtime/create-app.js +48 -4
- package/src/runtime/paths.js +27 -3
- package/src/runtime/service-manager.js +2 -2
- package/src/transport/telegram/bot.js +190 -77
- package/src/transport/telegram/media.js +17 -11
- package/tools/openai-transcribe/index.js +1 -1
- package/tools/schedule-agent-task/config.js +1 -0
- package/tools/schedule-agent-task/index.js +68 -0
- package/tools/schedule-agent-task/package.json +6 -0
- package/tools/schedule-agent-task/tool.manifest.json +8 -0
- package/tools/web-browser/index.js +1 -1
package/AGENTS.md
CHANGED
|
@@ -54,6 +54,17 @@ Every CLI must support:
|
|
|
54
54
|
- `node index.js --help`
|
|
55
55
|
- `node index.js run --request-file <json>`
|
|
56
56
|
|
|
57
|
+
### Tools that need daemons
|
|
58
|
+
Some tools need a persistent process, for example to keep a browser session alive or a local model warm.
|
|
59
|
+
Implement these tools with the shared daemon runtime instead of custom ad hoc process management:
|
|
60
|
+
- use `src/core/tools/daemon-runtime.js`
|
|
61
|
+
- keep runtime files under the tool state directory (`stateDir/<toolName>`)
|
|
62
|
+
- expose normal CLI behavior through `run --request-file`; callers should not manage daemon internals
|
|
63
|
+
- use the runtime for `daemon.pid`, `daemon.log`, `status.json`, and `commands/*.request|processing|result.json`
|
|
64
|
+
- keep one daemon owner per tool/session and avoid opening a second client over the same resource
|
|
65
|
+
- use `beforeStart` only for tool-specific cleanup such as stale browser locks, without deleting persistent session/model data
|
|
66
|
+
- keep daemon tools headless/server-safe by default when they are meant to run on VPS machines
|
|
67
|
+
|
|
57
68
|
## Pipe behavior in V1
|
|
58
69
|
V1 does not have a full automatic planner yet. The agent should:
|
|
59
70
|
1. understand whether the needed pipe belongs to pre-reasoning normalization or post-reasoning tool chaining
|
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.4`)
|
|
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.4 --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
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Flow genérico de eventos asíncronos para tools
|
|
2
|
+
|
|
3
|
+
> Estado: propuesta / no implementado. Guardado como referencia.
|
|
4
|
+
> La implementación actual (timer) se mantiene; este documento describe una evolución posible.
|
|
5
|
+
|
|
6
|
+
## Problema
|
|
7
|
+
|
|
8
|
+
Hoy la única re-entrada asíncrona al agente es por tiempo: una tool devuelve `asyncTask` con `runAt` y el poller de 1s en `src/transport/telegram/bot.js` lo dispara como prompt. Eso obliga a resolver con timer (polling crudo, latencia fija, re-spawn de la tool y un turno completo del agente en cada chequeo). Falta una **cola de eventos entrantes** que despierte al agente solo cuando hay algo que evaluar.
|
|
9
|
+
|
|
10
|
+
## Solución (polling ordenado por cola, reusando TaskStore)
|
|
11
|
+
|
|
12
|
+
Dos nuevos `kind` de tarea, drenados por el mismo poller hacia el mismo `enqueuePrompt`:
|
|
13
|
+
|
|
14
|
+
- `poll_tool`: tarea recurrente que el poller **ejecuta directamente como tool** (no gasta turno del agente). El checker mantiene su propio cursor de estado en su config/tmp por chat. Si hay novedad, emite un `agent_event`.
|
|
15
|
+
- `agent_event`: evento entrante que se dispara de inmediato. El poller lo entrega como prompt para que Pi lo evalúe y decida.
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart LR
|
|
19
|
+
Tool[Tool run normal] -->|asyncTask poll_tool| TS[TaskStore]
|
|
20
|
+
TS --> Poller[1s poller dispatcher]
|
|
21
|
+
Poller -->|kind poll_tool| Run[agentManager.runTool checker]
|
|
22
|
+
Run -->|si hay novedad: asyncTask agent_event| TS
|
|
23
|
+
Poller -->|kind agent_event| EP[enqueuePrompt]
|
|
24
|
+
Poller -->|kind agent_task| EP
|
|
25
|
+
EP --> Pi[Pi evalua y decide]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Cambios
|
|
29
|
+
|
|
30
|
+
### 1. TaskStore: eventos/polls sin hora se disparan ya
|
|
31
|
+
|
|
32
|
+
`src/core/tasks/task-store.js` - en `normalizeTask`, default `runAt` a `now` cuando no viene (los `agent_event` y el primer disparo de `poll_tool` deben ser inmediatos; `computeNextRunAt` ya reprograma `poll_tool` por su `recurrence`). Cambio de una línea, no rompe `agent_task` (siempre trae `runAt`).
|
|
33
|
+
|
|
34
|
+
### 2. AgentManager: extraer "run + materializar" (DRY)
|
|
35
|
+
|
|
36
|
+
`src/core/agent/agent-manager.js` - hoy el `execute` de `run_tool` (líneas ~184-242) hace: correr la tool, convertir `output.text`/`output.filePath` en artifacts y mandar `asyncTask(s)` al `TaskStore` con el `chatId`. Extraer eso a un método reusable `runTool({ name, request, chatId })`. El Pi tool `run_tool` pasa a llamarlo. Así el poller puede correr tools con la **misma** lógica de materialización (incluido el alta de `agent_event` que emita el checker).
|
|
37
|
+
|
|
38
|
+
### 3. Poller -> dispatcher por kind
|
|
39
|
+
|
|
40
|
+
`src/transport/telegram/bot.js` - reemplazar el handler de un solo kind dentro del `setInterval` (líneas ~361-380) por un dispatcher:
|
|
41
|
+
|
|
42
|
+
- `agent_task` -> `enqueuePrompt(buildAsyncTaskPrompt(task))` + `complete` (igual que hoy).
|
|
43
|
+
- `agent_event` -> `enqueuePrompt(buildAsyncEventPrompt(task))` + `complete`.
|
|
44
|
+
- `poll_tool` -> `agentManager.runTool({ name: task.payload.toolName, request: { args: task.payload.args || {} }, chatId })`; los `agent_event` que emita el checker quedan encolados para el próximo tick; luego `complete` (la `recurrence` reprograma el poll). Si la tool falla: log + `complete` para no matar el poll.
|
|
45
|
+
|
|
46
|
+
Agregar `buildAsyncEventPrompt(task)` junto a `buildAsyncTaskPrompt` (línea ~82), con framing de "llegó un evento externo, evalualo y decidí la próxima acción". Si el branch queda denso, extraer `dispatchDueTasks(...)` a una función para mantener `bot.js` como transporte.
|
|
47
|
+
|
|
48
|
+
### 4. Documentar el flow
|
|
49
|
+
|
|
50
|
+
`AGENTS.md` - sección nueva (en inglés) explicando: cómo una tool arma su auto-polling devolviendo un `asyncTask` kind `poll_tool` con `recurrence`, cómo emite novedades con `asyncTask` kind `agent_event`, que el checker guarda su cursor en su config/tmp por chat, y que el agente razona sobre el `agent_event` para decidir. `list_scheduled_tasks`/`cancel_scheduled_task` ya sirven (son kind-agnostic) para ver/cancelar polls.
|
|
51
|
+
|
|
52
|
+
## Contrato del checker tool (sin nuevas Pi tools)
|
|
53
|
+
|
|
54
|
+
Todo pasa por el campo `asyncTasks` que el pipeline ya soporta:
|
|
55
|
+
|
|
56
|
+
- Arranque del poll (desde el `run` de cualquier tool): `asyncTasks: [{ kind: "poll_tool", payload: { toolName, args }, recurrence: { type: "interval", everySeconds: N } }]`.
|
|
57
|
+
- Novedad (desde el `run` del checker): `asyncTasks: [{ kind: "agent_event", payload: { prompt: "<contenido a evaluar>" } }]`.
|
|
58
|
+
|
|
59
|
+
## No-goals (por ahora)
|
|
60
|
+
|
|
61
|
+
- No se agrega listener persistente (`node index.js listen`) ni proceso de fondo con IPC.
|
|
62
|
+
- No se agrega endpoint HTTP entrante para eventos.
|
|
63
|
+
- No se resuelve el caso de conexión sostenida (tipo cliente logueado): los checkers son one-shot y persisten su cursor entre corridas.
|
|
64
|
+
|
|
65
|
+
## Alternativas consideradas (descartadas para esta versión)
|
|
66
|
+
|
|
67
|
+
- **Listener tools**: la tool corre como proceso de larga duración (`node index.js listen`) y emite eventos por stdout que Arisa drena a la cola. Más general y realtime, pero agrega ciclo de vida de proceso a la service e IPC.
|
|
68
|
+
- **Webhook entrante**: Arisa expone un endpoint HTTP interno donde sistemas externos hacen POST de eventos. Bueno para callbacks; no sirve para los que requieren sostener una conexión.
|
package/package.json
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.2",
|
|
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",
|
|
@@ -30,12 +26,15 @@
|
|
|
30
26
|
"pi.dev",
|
|
31
27
|
"clasen"
|
|
32
28
|
],
|
|
33
|
-
"author": "",
|
|
34
|
-
"license": "
|
|
35
|
-
"packageManager": "pnpm@10.32.1",
|
|
29
|
+
"author": "Martin Clasen",
|
|
30
|
+
"license": "MIT",
|
|
36
31
|
"dependencies": {
|
|
37
|
-
"@mariozechner/pi-coding-agent": "^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,24 +5,54 @@ 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
|
-
constructor({ config, artifactStore, toolRegistry, logger }) {
|
|
25
|
+
constructor({ config, artifactStore, toolRegistry, taskStore, logger }) {
|
|
12
26
|
this.config = config;
|
|
13
27
|
this.artifactStore = artifactStore;
|
|
14
28
|
this.toolRegistry = toolRegistry;
|
|
29
|
+
this.taskStore = taskStore;
|
|
15
30
|
this.logger = logger;
|
|
16
31
|
this.sessions = new Map();
|
|
32
|
+
this.pendingNewSessions = new Set();
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
setConfig(config) {
|
|
20
36
|
this.config = config;
|
|
21
37
|
this.sessions.clear();
|
|
38
|
+
this.pendingNewSessions.clear();
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
resetSession(chatId) {
|
|
25
|
-
|
|
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 };
|
|
26
56
|
}
|
|
27
57
|
|
|
28
58
|
async validatePiAgent() {
|
|
@@ -35,7 +65,7 @@ export class AgentManager {
|
|
|
35
65
|
if (!model) {
|
|
36
66
|
throw new Error(`Model not found: ${this.config.pi.provider}/${this.config.pi.model}`);
|
|
37
67
|
}
|
|
38
|
-
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 })) {
|
|
39
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.`);
|
|
40
70
|
}
|
|
41
71
|
|
|
@@ -49,23 +79,33 @@ export class AgentManager {
|
|
|
49
79
|
}
|
|
50
80
|
|
|
51
81
|
async getSessionContext(chatId, telegram) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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);
|
|
55
93
|
}
|
|
56
94
|
|
|
57
95
|
const { authStorage, modelRegistry } = createPiRuntime({
|
|
58
96
|
provider: this.config.pi.provider,
|
|
59
97
|
apiKey: this.config.pi.apiKey
|
|
60
98
|
});
|
|
61
|
-
const model = modelRegistry.find(this.config.pi.provider,
|
|
62
|
-
if (!model) throw new Error(`Model not found: ${this.config.pi.provider}/${
|
|
63
|
-
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 })) {
|
|
64
102
|
throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
|
|
65
103
|
}
|
|
66
104
|
|
|
67
|
-
|
|
68
|
-
const
|
|
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}`);
|
|
108
|
+
const customTools = this.createTools(telegram, chatId);
|
|
69
109
|
const { session } = await createAgentSession({
|
|
70
110
|
cwd: arisaInstallDir,
|
|
71
111
|
agentDir: arisaHomeDir,
|
|
@@ -73,21 +113,28 @@ export class AgentManager {
|
|
|
73
113
|
modelRegistry,
|
|
74
114
|
model,
|
|
75
115
|
customTools,
|
|
76
|
-
sessionManager
|
|
116
|
+
sessionManager
|
|
77
117
|
});
|
|
78
118
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
}
|
|
84
126
|
|
|
85
|
-
const ctx = { session };
|
|
86
|
-
this.sessions.set(
|
|
127
|
+
const ctx = { session, modelId: effectiveModelId };
|
|
128
|
+
this.sessions.set(sessionKey, ctx);
|
|
129
|
+
if (isNewSession) {
|
|
130
|
+
this.pendingNewSessions.delete(sessionKey);
|
|
131
|
+
}
|
|
87
132
|
return ctx;
|
|
88
133
|
}
|
|
89
134
|
|
|
90
|
-
createTools(telegram) {
|
|
135
|
+
createTools(telegram, chatId) {
|
|
136
|
+
const chatArtifactStore = this.artifactStore.forChat(chatId);
|
|
137
|
+
|
|
91
138
|
return [
|
|
92
139
|
defineTool({
|
|
93
140
|
name: "list_tools",
|
|
@@ -116,11 +163,11 @@ export class AgentManager {
|
|
|
116
163
|
defineTool({
|
|
117
164
|
name: "set_tool_config",
|
|
118
165
|
label: "Set tool config",
|
|
119
|
-
description: "Write a value
|
|
166
|
+
description: "Write a tool config value scoped to the current chat.",
|
|
120
167
|
parameters: Type.Object({ name: Type.String(), field: Type.String(), value: Type.String() }),
|
|
121
168
|
execute: async (_id, params) => {
|
|
122
169
|
await this.toolRegistry.load();
|
|
123
|
-
const result = await this.toolRegistry.setConfig(params.name, params.field, params.value);
|
|
170
|
+
const result = await this.toolRegistry.setConfig(params.name, params.field, params.value, chatId);
|
|
124
171
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
125
172
|
}
|
|
126
173
|
}),
|
|
@@ -139,7 +186,7 @@ export class AgentManager {
|
|
|
139
186
|
this.logger?.log("agent", `run_tool ${params.name}`);
|
|
140
187
|
let artifact = null;
|
|
141
188
|
if (params.artifactId) {
|
|
142
|
-
artifact = await
|
|
189
|
+
artifact = await chatArtifactStore.get(params.artifactId);
|
|
143
190
|
if (!artifact) {
|
|
144
191
|
return { content: [{ type: "text", text: `Artifact not found: ${params.artifactId}` }], details: { ok: false } };
|
|
145
192
|
}
|
|
@@ -150,11 +197,12 @@ export class AgentManager {
|
|
|
150
197
|
artifact,
|
|
151
198
|
text: params.text,
|
|
152
199
|
args: params.args || {}
|
|
153
|
-
}
|
|
200
|
+
},
|
|
201
|
+
chatId
|
|
154
202
|
});
|
|
155
203
|
|
|
156
204
|
if (result.output?.text) {
|
|
157
|
-
const outArtifact = await
|
|
205
|
+
const outArtifact = await chatArtifactStore.createText({
|
|
158
206
|
text: result.output.text,
|
|
159
207
|
source: { type: "tool", toolName: params.name },
|
|
160
208
|
metadata: { tool: params.name }
|
|
@@ -163,7 +211,7 @@ export class AgentManager {
|
|
|
163
211
|
}
|
|
164
212
|
|
|
165
213
|
if (result.output?.filePath) {
|
|
166
|
-
const generated = await
|
|
214
|
+
const generated = await chatArtifactStore.createFromFile({
|
|
167
215
|
originalPath: result.output.filePath,
|
|
168
216
|
fileName: result.output.fileName || path.basename(result.output.filePath),
|
|
169
217
|
kind: result.output.kind || "file",
|
|
@@ -175,12 +223,72 @@ export class AgentManager {
|
|
|
175
223
|
await unlink(result.output.filePath).catch(() => {});
|
|
176
224
|
}
|
|
177
225
|
|
|
226
|
+
if (result.asyncTask || result.asyncTasks?.length) {
|
|
227
|
+
const scheduled = await this.taskStore.addMany(
|
|
228
|
+
result.asyncTasks || [result.asyncTask],
|
|
229
|
+
{
|
|
230
|
+
payload: { chatId },
|
|
231
|
+
source: { type: "tool", toolName: params.name, chatId }
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
result.asyncTasks = scheduled;
|
|
235
|
+
delete result.asyncTask;
|
|
236
|
+
}
|
|
237
|
+
|
|
178
238
|
return {
|
|
179
239
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
180
240
|
details: result
|
|
181
241
|
};
|
|
182
242
|
}
|
|
183
243
|
}),
|
|
244
|
+
defineTool({
|
|
245
|
+
name: "list_scheduled_tasks",
|
|
246
|
+
label: "List scheduled tasks",
|
|
247
|
+
description: "List scheduled async tasks for the current Telegram chat.",
|
|
248
|
+
parameters: Type.Object({
|
|
249
|
+
status: Type.Optional(Type.String())
|
|
250
|
+
}),
|
|
251
|
+
execute: async (_id, params) => {
|
|
252
|
+
const tasks = await this.taskStore.list({ chatId, status: params.status });
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }],
|
|
255
|
+
details: { tasks }
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}),
|
|
259
|
+
defineTool({
|
|
260
|
+
name: "cancel_scheduled_task",
|
|
261
|
+
label: "Cancel scheduled task",
|
|
262
|
+
description: "Cancel one scheduled async task by id for the current Telegram chat.",
|
|
263
|
+
parameters: Type.Object({ id: Type.String() }),
|
|
264
|
+
execute: async (_id, params) => {
|
|
265
|
+
const existing = await this.taskStore.get(params.id);
|
|
266
|
+
if (!existing || existing.payload?.chatId !== chatId) {
|
|
267
|
+
return {
|
|
268
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Task not found" }) }],
|
|
269
|
+
details: { ok: false, error: "Task not found" }
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const task = await this.taskStore.cancel(params.id);
|
|
273
|
+
return {
|
|
274
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, task }, null, 2) }],
|
|
275
|
+
details: { ok: true, task }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}),
|
|
279
|
+
defineTool({
|
|
280
|
+
name: "cancel_all_scheduled_tasks",
|
|
281
|
+
label: "Cancel all scheduled tasks",
|
|
282
|
+
description: "Cancel all pending or running async tasks for the current Telegram chat.",
|
|
283
|
+
parameters: Type.Object({}),
|
|
284
|
+
execute: async () => {
|
|
285
|
+
const tasks = await this.taskStore.cancelAll({ chatId });
|
|
286
|
+
return {
|
|
287
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, cancelled: tasks.length }, null, 2) }],
|
|
288
|
+
details: { ok: true, tasks }
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}),
|
|
184
292
|
defineTool({
|
|
185
293
|
name: "send_media_reply",
|
|
186
294
|
label: "Send media reply",
|
|
@@ -200,7 +308,8 @@ export class AgentManager {
|
|
|
200
308
|
this.logger?.log("agent", `send_media_reply via ${toolName}`);
|
|
201
309
|
const result = await this.toolRegistry.run({
|
|
202
310
|
name: toolName,
|
|
203
|
-
request: { text: params.text, args: {} }
|
|
311
|
+
request: { text: params.text, args: {} },
|
|
312
|
+
chatId
|
|
204
313
|
});
|
|
205
314
|
if (!result.ok || !result.output?.filePath) {
|
|
206
315
|
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,
|
|
2
|
+
import { arisaHomeDir, chatsDir, stateDir, 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,7 @@ export function buildAgentRuntimeContext() {
|
|
|
10
10
|
`arisaInstallDir: ${arisaInstallDir}`,
|
|
11
11
|
`bundledToolsDir: ${bundledToolsDir}`,
|
|
12
12
|
`userToolsDir: ${toolsDir}`,
|
|
13
|
-
`
|
|
13
|
+
`chatsDir: ${chatsDir}`,
|
|
14
14
|
`stateDir: ${stateDir}`
|
|
15
15
|
].join("\n");
|
|
16
16
|
}
|
|
@@ -1,39 +1,44 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
async function loadIndex() {
|
|
7
|
-
try {
|
|
8
|
-
return JSON.parse(await readFile(indexFile, "utf8"));
|
|
9
|
-
} catch {
|
|
10
|
-
return [];
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function saveIndex(items) {
|
|
15
|
-
await mkdir(path.dirname(indexFile), { recursive: true });
|
|
16
|
-
await writeFile(indexFile, `${JSON.stringify(items, null, 2)}\n`, "utf8");
|
|
17
|
-
}
|
|
4
|
+
import { getChatArtifactsDir, getChatArtifactsIndexFile } from "../../runtime/paths.js";
|
|
18
5
|
|
|
19
6
|
function id() {
|
|
20
7
|
return crypto.randomUUID();
|
|
21
8
|
}
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
constructor() {
|
|
10
|
+
class ChatArtifactStore {
|
|
11
|
+
constructor(chatId) {
|
|
12
|
+
this.chatId = String(chatId);
|
|
13
|
+
this.rootDir = getChatArtifactsDir(this.chatId);
|
|
14
|
+
this.indexFile = getChatArtifactsIndexFile(this.chatId);
|
|
25
15
|
this.items = null;
|
|
26
16
|
}
|
|
27
17
|
|
|
18
|
+
async reload() {
|
|
19
|
+
try {
|
|
20
|
+
this.items = JSON.parse(await readFile(this.indexFile, "utf8"));
|
|
21
|
+
} catch {
|
|
22
|
+
this.items = [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
28
26
|
async init() {
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
await mkdir(this.rootDir, { recursive: true });
|
|
28
|
+
if (!this.items) await this.reload();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async saveIndex() {
|
|
32
|
+
await mkdir(path.dirname(this.indexFile), { recursive: true });
|
|
33
|
+
await writeFile(this.indexFile, `${JSON.stringify(this.items, null, 2)}\n`, "utf8");
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
async createText({ text, mimeType = "text/plain", source, metadata = {} }) {
|
|
34
37
|
await this.init();
|
|
38
|
+
await this.reload();
|
|
35
39
|
const artifact = {
|
|
36
40
|
id: id(),
|
|
41
|
+
chatId: this.chatId,
|
|
37
42
|
kind: "text",
|
|
38
43
|
mimeType,
|
|
39
44
|
text,
|
|
@@ -42,19 +47,21 @@ export class ArtifactStore {
|
|
|
42
47
|
createdAt: new Date().toISOString()
|
|
43
48
|
};
|
|
44
49
|
this.items.push(artifact);
|
|
45
|
-
await saveIndex(
|
|
50
|
+
await this.saveIndex();
|
|
46
51
|
return artifact;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
async createFromFile({ originalPath, fileName, kind, mimeType, source, metadata = {} }) {
|
|
50
55
|
await this.init();
|
|
56
|
+
await this.reload();
|
|
51
57
|
const artifactId = id();
|
|
52
|
-
const dir = path.join(rootDir, artifactId);
|
|
58
|
+
const dir = path.join(this.rootDir, artifactId);
|
|
53
59
|
await mkdir(dir, { recursive: true });
|
|
54
60
|
const destPath = path.join(dir, fileName);
|
|
55
61
|
await copyFile(originalPath, destPath);
|
|
56
62
|
const artifact = {
|
|
57
63
|
id: artifactId,
|
|
64
|
+
chatId: this.chatId,
|
|
58
65
|
kind,
|
|
59
66
|
mimeType,
|
|
60
67
|
path: destPath,
|
|
@@ -63,19 +70,21 @@ export class ArtifactStore {
|
|
|
63
70
|
createdAt: new Date().toISOString()
|
|
64
71
|
};
|
|
65
72
|
this.items.push(artifact);
|
|
66
|
-
await saveIndex(
|
|
73
|
+
await this.saveIndex();
|
|
67
74
|
return artifact;
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
async createGeneratedFile({ fileName, content, kind, mimeType, source, metadata = {} }) {
|
|
71
78
|
await this.init();
|
|
79
|
+
await this.reload();
|
|
72
80
|
const artifactId = id();
|
|
73
|
-
const dir = path.join(rootDir, artifactId);
|
|
81
|
+
const dir = path.join(this.rootDir, artifactId);
|
|
74
82
|
await mkdir(dir, { recursive: true });
|
|
75
83
|
const destPath = path.join(dir, fileName);
|
|
76
84
|
await writeFile(destPath, content);
|
|
77
85
|
const artifact = {
|
|
78
86
|
id: artifactId,
|
|
87
|
+
chatId: this.chatId,
|
|
79
88
|
kind,
|
|
80
89
|
mimeType,
|
|
81
90
|
path: destPath,
|
|
@@ -84,17 +93,33 @@ export class ArtifactStore {
|
|
|
84
93
|
createdAt: new Date().toISOString()
|
|
85
94
|
};
|
|
86
95
|
this.items.push(artifact);
|
|
87
|
-
await saveIndex(
|
|
96
|
+
await this.saveIndex();
|
|
88
97
|
return artifact;
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
async get(
|
|
100
|
+
async get(artifactId) {
|
|
92
101
|
await this.init();
|
|
93
|
-
|
|
102
|
+
await this.reload();
|
|
103
|
+
return this.items.find((item) => item.id === artifactId) || null;
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
async listRecent(limit = 20) {
|
|
97
107
|
await this.init();
|
|
108
|
+
await this.reload();
|
|
98
109
|
return [...this.items].slice(-limit).reverse();
|
|
99
110
|
}
|
|
100
111
|
}
|
|
112
|
+
|
|
113
|
+
export class ArtifactStore {
|
|
114
|
+
constructor() {
|
|
115
|
+
this.chatStores = new Map();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
forChat(chatId) {
|
|
119
|
+
const key = String(chatId);
|
|
120
|
+
if (!this.chatStores.has(key)) {
|
|
121
|
+
this.chatStores.set(key, new ChatArtifactStore(key));
|
|
122
|
+
}
|
|
123
|
+
return this.chatStores.get(key);
|
|
124
|
+
}
|
|
125
|
+
}
|