arisa 3.1.4 → 3.1.8
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/package.json +1 -1
- package/src/core/agent/agent-manager.js +49 -8
- package/src/core/agent/prompt-timeout.js +22 -0
- package/src/core/tools/daemon-processes.js +133 -0
- package/src/core/tools/daemon-runtime.js +14 -51
- package/src/index.js +42 -6
- package/src/runtime/create-app.js +14 -1
- package/src/runtime/service-manager.js +4 -14
- package/src/runtime/tool-process-supervisor.js +63 -0
- package/src/transport/telegram/bot.js +82 -17
- package/tools/openai-tts/index.js +4 -2
- package/src/core/agent/project-instructions.js +0 -11
package/package.json
CHANGED
|
@@ -3,10 +3,12 @@ import { unlink } from "node:fs/promises";
|
|
|
3
3
|
import { createAgentSession, SessionManager, defineTool } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
|
|
6
|
-
import { loadProjectInstructions } from "./project-instructions.js";
|
|
7
6
|
import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js";
|
|
7
|
+
import { withTimeout } from "./prompt-timeout.js";
|
|
8
8
|
import { arisaHomeDir, getChatPiSessionsDir } from "../../runtime/paths.js";
|
|
9
9
|
|
|
10
|
+
const piValidationTimeoutMs = 60_000;
|
|
11
|
+
|
|
10
12
|
function isLocalBaseUrl(value) {
|
|
11
13
|
if (typeof value !== "string" || !value.trim()) return false;
|
|
12
14
|
try {
|
|
@@ -21,6 +23,32 @@ function requiresProviderAuth(model) {
|
|
|
21
23
|
return !isLocalBaseUrl(model?.baseUrl);
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
function mimeMatches(pattern, mimeType = "") {
|
|
27
|
+
if (!pattern || !mimeType) return false;
|
|
28
|
+
if (pattern === mimeType) return true;
|
|
29
|
+
if (pattern.endsWith("/*")) return mimeType.startsWith(`${pattern.slice(0, -2)}/`);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toolSupportsTextInput(tool) {
|
|
34
|
+
return (tool.input || []).some((input) => mimeMatches(input, "text/plain"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toolProducesAudio(tool) {
|
|
38
|
+
return (tool.output || []).some((output) => mimeMatches(output, "audio/ogg") || mimeMatches(output, "audio/*"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function looksLikeTextToSpeechTool(tool) {
|
|
42
|
+
return /tts|text.?to.?speech|speech.?audio|speech/i.test(`${tool.name} ${tool.description || ""}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function selectMediaReplyTool(toolRegistry) {
|
|
46
|
+
const tools = toolRegistry.list()
|
|
47
|
+
.filter(toolSupportsTextInput)
|
|
48
|
+
.filter(toolProducesAudio);
|
|
49
|
+
return tools.find(looksLikeTextToSpeechTool) || tools[0] || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
24
52
|
export class AgentManager {
|
|
25
53
|
constructor({ config, artifactStore, toolRegistry, taskStore, logger }) {
|
|
26
54
|
this.config = config;
|
|
@@ -75,7 +103,10 @@ export class AgentManager {
|
|
|
75
103
|
model,
|
|
76
104
|
sessionManager: SessionManager.inMemory(),
|
|
77
105
|
});
|
|
78
|
-
await session.prompt("Reply with exactly: OK")
|
|
106
|
+
await withTimeout(session.prompt("Reply with exactly: OK"), {
|
|
107
|
+
timeoutMs: piValidationTimeoutMs,
|
|
108
|
+
label: "Pi validation prompt"
|
|
109
|
+
});
|
|
79
110
|
}
|
|
80
111
|
|
|
81
112
|
async getSessionContext(chatId, telegram) {
|
|
@@ -117,11 +148,8 @@ export class AgentManager {
|
|
|
117
148
|
});
|
|
118
149
|
|
|
119
150
|
if (!hasExistingSession) {
|
|
120
|
-
|
|
121
|
-
|
|
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`);
|
|
151
|
+
this.logger?.log("agent", `created new session for chat ${sessionKey}`);
|
|
152
|
+
this.logger?.log("agent", `runtime context for chat ${sessionKey}:\n${buildAgentRuntimeContext()}`);
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
const ctx = { session, modelId: effectiveModelId };
|
|
@@ -323,7 +351,20 @@ export class AgentManager {
|
|
|
323
351
|
}),
|
|
324
352
|
execute: async (_id, params) => {
|
|
325
353
|
await this.toolRegistry.load();
|
|
326
|
-
const
|
|
354
|
+
const selectedTool = params.toolName
|
|
355
|
+
? this.toolRegistry.get(params.toolName)
|
|
356
|
+
: selectMediaReplyTool(this.toolRegistry);
|
|
357
|
+
if (!selectedTool) {
|
|
358
|
+
const result = {
|
|
359
|
+
ok: false,
|
|
360
|
+
status: "failed",
|
|
361
|
+
error: params.toolName
|
|
362
|
+
? `Tool not found: ${params.toolName}`
|
|
363
|
+
: "No registered text-to-speech tool can generate an audio reply."
|
|
364
|
+
};
|
|
365
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
|
|
366
|
+
}
|
|
367
|
+
const toolName = selectedTool.name;
|
|
327
368
|
this.logger?.log("agent", `send_media_reply via ${toolName}`);
|
|
328
369
|
const result = await this.toolRegistry.run({
|
|
329
370
|
name: toolName,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class PromptTimeoutError extends Error {
|
|
2
|
+
constructor(label, timeoutMs) {
|
|
3
|
+
super(`${label} timed out after ${timeoutMs}ms`);
|
|
4
|
+
this.name = "PromptTimeoutError";
|
|
5
|
+
this.timeoutMs = timeoutMs;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function withTimeout(promise, { timeoutMs, label }) {
|
|
10
|
+
let timer = null;
|
|
11
|
+
const timeout = new Promise((_, reject) => {
|
|
12
|
+
timer = setTimeout(() => {
|
|
13
|
+
reject(new PromptTimeoutError(label, timeoutMs));
|
|
14
|
+
}, timeoutMs);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
return await Promise.race([promise, timeout]);
|
|
19
|
+
} finally {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, openSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getToolStateDir, toolStateDir } from "../../runtime/paths.js";
|
|
6
|
+
|
|
7
|
+
export function daemonPaths(toolName) {
|
|
8
|
+
const root = getToolStateDir(toolName);
|
|
9
|
+
return {
|
|
10
|
+
root,
|
|
11
|
+
commandsDir: path.join(root, "commands"),
|
|
12
|
+
pidFile: path.join(root, "daemon.pid"),
|
|
13
|
+
metaFile: path.join(root, "daemon.meta.json"),
|
|
14
|
+
statusFile: path.join(root, "status.json"),
|
|
15
|
+
logFile: path.join(root, "daemon.log")
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function readJson(file, fallback = {}) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
22
|
+
} catch {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function writeJson(file, value) {
|
|
28
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
29
|
+
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isProcessAlive(pid) {
|
|
33
|
+
if (!pid) return false;
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function waitForExit(pid, timeoutMs) {
|
|
43
|
+
const startedAt = Date.now();
|
|
44
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
45
|
+
if (!isProcessAlive(pid)) return true;
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
47
|
+
}
|
|
48
|
+
return !isProcessAlive(pid);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function stopManagedDaemon(toolName, { signal = "SIGTERM", forceAfterMs = 3000 } = {}) {
|
|
52
|
+
const paths = daemonPaths(toolName);
|
|
53
|
+
const { pid } = await readJson(paths.pidFile, {});
|
|
54
|
+
let stopped = false;
|
|
55
|
+
if (isProcessAlive(pid)) {
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, signal);
|
|
58
|
+
stopped = true;
|
|
59
|
+
} catch {}
|
|
60
|
+
if (signal !== "SIGKILL" && forceAfterMs > 0 && !(await waitForExit(pid, forceAfterMs))) {
|
|
61
|
+
try {
|
|
62
|
+
process.kill(pid, "SIGKILL");
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await rm(paths.pidFile, { force: true });
|
|
67
|
+
return { toolName, pid: pid || null, stopped };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function startManagedDaemon({ toolName, entryPath, beforeStart = null, autoStart = true }) {
|
|
71
|
+
const paths = daemonPaths(toolName);
|
|
72
|
+
await mkdir(paths.commandsDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
const current = await readJson(paths.pidFile, {});
|
|
75
|
+
if (isProcessAlive(current.pid)) {
|
|
76
|
+
await writeJson(paths.metaFile, {
|
|
77
|
+
toolName,
|
|
78
|
+
entryPath,
|
|
79
|
+
autoStart,
|
|
80
|
+
lastStartedAt: current.startedAt || new Date().toISOString()
|
|
81
|
+
});
|
|
82
|
+
return current.pid;
|
|
83
|
+
}
|
|
84
|
+
await rm(paths.pidFile, { force: true });
|
|
85
|
+
|
|
86
|
+
await rm(paths.commandsDir, { recursive: true, force: true });
|
|
87
|
+
await mkdir(paths.commandsDir, { recursive: true });
|
|
88
|
+
if (beforeStart) await beforeStart();
|
|
89
|
+
|
|
90
|
+
const out = openSync(paths.logFile, "a");
|
|
91
|
+
try {
|
|
92
|
+
const child = spawn(process.execPath, [entryPath, "daemon"], {
|
|
93
|
+
detached: false,
|
|
94
|
+
stdio: ["ignore", out, out],
|
|
95
|
+
env: process.env
|
|
96
|
+
});
|
|
97
|
+
child.unref();
|
|
98
|
+
|
|
99
|
+
const startedAt = new Date().toISOString();
|
|
100
|
+
const record = {
|
|
101
|
+
pid: child.pid,
|
|
102
|
+
startedAt
|
|
103
|
+
};
|
|
104
|
+
await writeJson(paths.pidFile, record);
|
|
105
|
+
await writeJson(paths.metaFile, {
|
|
106
|
+
toolName,
|
|
107
|
+
entryPath,
|
|
108
|
+
autoStart,
|
|
109
|
+
lastStartedAt: startedAt
|
|
110
|
+
});
|
|
111
|
+
return child.pid;
|
|
112
|
+
} finally {
|
|
113
|
+
closeSync(out);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function listRegisteredDaemons() {
|
|
118
|
+
let entries = [];
|
|
119
|
+
try {
|
|
120
|
+
entries = await readdir(toolStateDir, { withFileTypes: true });
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const records = [];
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (!entry.isDirectory()) continue;
|
|
128
|
+
const paths = daemonPaths(entry.name);
|
|
129
|
+
const meta = await readJson(paths.metaFile, null);
|
|
130
|
+
if (meta?.toolName && meta?.entryPath) records.push(meta);
|
|
131
|
+
}
|
|
132
|
+
return records;
|
|
133
|
+
}
|
|
@@ -1,34 +1,14 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { openSync } from "node:fs";
|
|
4
|
-
import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readdir, rename, unlink } from "node:fs/promises";
|
|
5
3
|
import path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
statusFile: path.join(root, "status.json"),
|
|
15
|
-
logFile: path.join(root, "daemon.log")
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function readJson(file, fallback = {}) {
|
|
20
|
-
try { return JSON.parse(await readFile(file, "utf8")); } catch { return fallback; }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function writeJson(file, value) {
|
|
24
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
25
|
-
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isProcessAlive(pid) {
|
|
29
|
-
if (!pid) return false;
|
|
30
|
-
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
31
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
daemonPaths,
|
|
6
|
+
isProcessAlive,
|
|
7
|
+
readJson,
|
|
8
|
+
startManagedDaemon,
|
|
9
|
+
stopManagedDaemon,
|
|
10
|
+
writeJson
|
|
11
|
+
} from "./daemon-processes.js";
|
|
32
12
|
|
|
33
13
|
export function createDaemonRuntime({ toolName, entryPath, beforeStart = null }) {
|
|
34
14
|
const paths = daemonPaths(toolName);
|
|
@@ -54,33 +34,16 @@ export function createDaemonRuntime({ toolName, entryPath, beforeStart = null })
|
|
|
54
34
|
await writeJson(paths.statusFile, { ...current, ...patch, updatedAt: new Date().toISOString() });
|
|
55
35
|
}
|
|
56
36
|
|
|
57
|
-
async function clearJobs() {
|
|
58
|
-
await rm(paths.commandsDir, { recursive: true, force: true });
|
|
59
|
-
await mkdir(paths.commandsDir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
37
|
async function start() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
await clearJobs();
|
|
68
|
-
if (beforeStart) await beforeStart();
|
|
69
|
-
const out = openSync(paths.logFile, "a");
|
|
70
|
-
const child = spawn(process.execPath, [entryPath, "daemon"], {
|
|
71
|
-
detached: true,
|
|
72
|
-
stdio: ["ignore", out, out],
|
|
73
|
-
env: process.env
|
|
38
|
+
return startManagedDaemon({
|
|
39
|
+
toolName,
|
|
40
|
+
entryPath,
|
|
41
|
+
beforeStart
|
|
74
42
|
});
|
|
75
|
-
child.unref();
|
|
76
|
-
await writeJson(paths.pidFile, { pid: child.pid, startedAt: new Date().toISOString() });
|
|
77
|
-
return child.pid;
|
|
78
43
|
}
|
|
79
44
|
|
|
80
45
|
async function stop() {
|
|
81
|
-
|
|
82
|
-
if (isProcessAlive(pid)) process.kill(pid, "SIGTERM");
|
|
83
|
-
await rm(paths.pidFile, { force: true });
|
|
46
|
+
await stopManagedDaemon(toolName);
|
|
84
47
|
}
|
|
85
48
|
|
|
86
49
|
async function waitReady({ timeoutMs = 120000, readyStates = ["ready"] } = {}) {
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createServer } from "node:http";
|
|
|
4
4
|
import { bootstrapIfNeeded } from "./runtime/bootstrap.js";
|
|
5
5
|
import { createApp } from "./runtime/create-app.js";
|
|
6
6
|
import { createLogger } from "./runtime/logger.js";
|
|
7
|
-
import { getServiceStatus, registerServiceProcess, startService, stopService } from "./runtime/service-manager.js";
|
|
7
|
+
import { getServiceStatus, registerServiceProcess, startService, stopService, unregisterServiceProcess } from "./runtime/service-manager.js";
|
|
8
8
|
import { flushArisaHome } from "./runtime/flush.js";
|
|
9
9
|
import { installPiPackage, removePiPackage } from "./runtime/pi-package-manager.js";
|
|
10
10
|
|
|
@@ -17,6 +17,8 @@ const serviceRunner = Boolean(cli.flags["service-runner"]);
|
|
|
17
17
|
const bootstrapOverrides = toBootstrapOverrides(cli.nestedFlags);
|
|
18
18
|
const runtimeOverrides = toRuntimeOverrides(cli.nestedFlags);
|
|
19
19
|
const logger = createLogger({ verbose });
|
|
20
|
+
let activeApp = null;
|
|
21
|
+
let shuttingDown = false;
|
|
20
22
|
|
|
21
23
|
const httpPort = Number(process.env.PORT);
|
|
22
24
|
let httpRequestHandler = null;
|
|
@@ -97,6 +99,35 @@ const bootstrapHttpOptions = httpPort ? { httpPort, setHttpRequestHandler } : {}
|
|
|
97
99
|
const webhookUrl = bootstrapOverrides.webhook?.url || "";
|
|
98
100
|
const appHttpOptions = httpPort ? { webhookUrl, setHttpRequestHandler } : {};
|
|
99
101
|
|
|
102
|
+
async function shutdown(exitCode = 0) {
|
|
103
|
+
if (shuttingDown) return;
|
|
104
|
+
shuttingDown = true;
|
|
105
|
+
try {
|
|
106
|
+
await activeApp?.stop?.();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
logger.error("app", `shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
exitCode = exitCode || 1;
|
|
110
|
+
}
|
|
111
|
+
if (serviceRunner) {
|
|
112
|
+
await unregisterServiceProcess();
|
|
113
|
+
}
|
|
114
|
+
process.exit(exitCode);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
process.once("SIGTERM", () => {
|
|
118
|
+
shutdown(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
process.once("SIGINT", () => {
|
|
122
|
+
shutdown(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
async function startRuntimeApp() {
|
|
126
|
+
const app = await createApp({ logger, runtimeOverrides, ...appHttpOptions });
|
|
127
|
+
activeApp = app;
|
|
128
|
+
await app.start();
|
|
129
|
+
}
|
|
130
|
+
|
|
100
131
|
async function runForeground() {
|
|
101
132
|
const hasRuntimePiOverrides = Boolean(
|
|
102
133
|
runtimeOverrides?.pi?.model
|
|
@@ -106,11 +137,12 @@ async function runForeground() {
|
|
|
106
137
|
logger.log("app", `starting${verbose ? " in verbose mode" : ""}`);
|
|
107
138
|
await bootstrapIfNeeded({ force: forceBootstrap, cliConfigOverrides: bootstrapOverrides, ...bootstrapHttpOptions });
|
|
108
139
|
try {
|
|
109
|
-
|
|
110
|
-
await app.start();
|
|
140
|
+
await startRuntimeApp();
|
|
111
141
|
} catch (error) {
|
|
112
142
|
const message = error instanceof Error ? error.message : String(error);
|
|
113
143
|
if (message.includes("No auth found")) {
|
|
144
|
+
await activeApp?.stop?.();
|
|
145
|
+
activeApp = null;
|
|
114
146
|
console.log(`\n${message}\n`);
|
|
115
147
|
if (hasRuntimePiOverrides) {
|
|
116
148
|
console.log("Skipping automatic bootstrap because Pi runtime overrides were provided.");
|
|
@@ -119,8 +151,7 @@ async function runForeground() {
|
|
|
119
151
|
}
|
|
120
152
|
console.log("Reopening bootstrap so you can provide a Pi API key or switch to a provider you already authenticated with.\n");
|
|
121
153
|
await bootstrapIfNeeded({ force: true, cliConfigOverrides: bootstrapOverrides, ...bootstrapHttpOptions });
|
|
122
|
-
|
|
123
|
-
await app.start();
|
|
154
|
+
await startRuntimeApp();
|
|
124
155
|
return;
|
|
125
156
|
}
|
|
126
157
|
throw error;
|
|
@@ -202,4 +233,9 @@ async function main() {
|
|
|
202
233
|
await runForeground();
|
|
203
234
|
}
|
|
204
235
|
|
|
205
|
-
|
|
236
|
+
try {
|
|
237
|
+
await main();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logger.error("app", error instanceof Error ? error.message : String(error));
|
|
240
|
+
await shutdown(1);
|
|
241
|
+
}
|
|
@@ -4,6 +4,7 @@ import { ToolRegistry } from "../core/tools/tool-registry.js";
|
|
|
4
4
|
import { TaskStore } from "../core/tasks/task-store.js";
|
|
5
5
|
import { AgentManager } from "../core/agent/agent-manager.js";
|
|
6
6
|
import { createTelegramBot } from "../transport/telegram/bot.js";
|
|
7
|
+
import { createToolProcessSupervisor } from "./tool-process-supervisor.js";
|
|
7
8
|
|
|
8
9
|
function normalizeString(value) {
|
|
9
10
|
const text = String(value ?? "").trim();
|
|
@@ -51,6 +52,7 @@ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpR
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const artifactStore = new ArtifactStore();
|
|
55
|
+
const toolProcessSupervisor = createToolProcessSupervisor({ logger });
|
|
54
56
|
const toolRegistry = new ToolRegistry({ logger });
|
|
55
57
|
const taskStore = new TaskStore();
|
|
56
58
|
await toolRegistry.load();
|
|
@@ -63,8 +65,19 @@ export async function createApp({ logger, runtimeOverrides, webhookUrl, setHttpR
|
|
|
63
65
|
async start() {
|
|
64
66
|
logger?.log("app", `validating Pi model ${config.pi.provider}/${config.pi.model}`);
|
|
65
67
|
await agentManager.validatePiAgent();
|
|
68
|
+
await toolProcessSupervisor.start();
|
|
66
69
|
logger?.log("app", "starting Telegram bot");
|
|
67
|
-
|
|
70
|
+
try {
|
|
71
|
+
await bot.start();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
await toolProcessSupervisor.stop();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async stop() {
|
|
79
|
+
await bot.stop?.();
|
|
80
|
+
await toolProcessSupervisor.stop();
|
|
68
81
|
}
|
|
69
82
|
};
|
|
70
83
|
}
|
|
@@ -74,24 +74,14 @@ export async function stopService() {
|
|
|
74
74
|
return { ok: true, pid: status.pid };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export async function unregisterServiceProcess() {
|
|
78
|
+
await rm(servicePidFile, { force: true }).catch(() => {});
|
|
79
|
+
}
|
|
80
|
+
|
|
77
81
|
export async function registerServiceProcess() {
|
|
78
82
|
await ensureArisaHome();
|
|
79
83
|
await writeFile(servicePidFile, `${process.pid}\n`, "utf8");
|
|
80
84
|
|
|
81
|
-
const cleanup = async () => {
|
|
82
|
-
await rm(servicePidFile, { force: true }).catch(() => {});
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
process.on("SIGTERM", async () => {
|
|
86
|
-
await cleanup();
|
|
87
|
-
process.exit(0);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
process.on("SIGINT", async () => {
|
|
91
|
-
await cleanup();
|
|
92
|
-
process.exit(0);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
85
|
process.on("exit", () => {
|
|
96
86
|
rm(servicePidFile, { force: true }).catch(() => {});
|
|
97
87
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { access, rm } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
daemonPaths,
|
|
4
|
+
isProcessAlive,
|
|
5
|
+
listRegisteredDaemons,
|
|
6
|
+
readJson,
|
|
7
|
+
startManagedDaemon
|
|
8
|
+
} from "../core/tools/daemon-processes.js";
|
|
9
|
+
import { ensureArisaHome } from "./paths.js";
|
|
10
|
+
|
|
11
|
+
async function fileExists(file) {
|
|
12
|
+
try {
|
|
13
|
+
await access(file);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createToolProcessSupervisor({ logger } = {}) {
|
|
21
|
+
let running = false;
|
|
22
|
+
|
|
23
|
+
async function reconcileDaemons() {
|
|
24
|
+
await ensureArisaHome();
|
|
25
|
+
for (const record of await listRegisteredDaemons()) {
|
|
26
|
+
if (!record.autoStart) continue;
|
|
27
|
+
if (!(await fileExists(record.entryPath))) {
|
|
28
|
+
logger?.log("tools", `skipping daemon ${record.toolName}: missing entry ${record.entryPath}`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const paths = daemonPaths(record.toolName);
|
|
33
|
+
const { pid } = await readJson(paths.pidFile, {});
|
|
34
|
+
if (isProcessAlive(pid)) {
|
|
35
|
+
logger?.log("tools", `adopted managed daemon ${record.toolName} (pid ${pid})`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (pid) {
|
|
39
|
+
await rm(paths.pidFile, { force: true });
|
|
40
|
+
logger?.log("tools", `removed stale daemon pid for ${record.toolName} (${pid})`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
logger?.log("tools", `starting managed daemon ${record.toolName}`);
|
|
44
|
+
await startManagedDaemon({
|
|
45
|
+
toolName: record.toolName,
|
|
46
|
+
entryPath: record.entryPath
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
async start() {
|
|
53
|
+
if (running) return;
|
|
54
|
+
running = true;
|
|
55
|
+
await reconcileDaemons();
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async stop() {
|
|
59
|
+
if (!running) return;
|
|
60
|
+
running = false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -3,8 +3,11 @@ import path from "node:path";
|
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
4
|
import { captureIncomingArtifact } from "./media.js";
|
|
5
5
|
import { renderTelegramHtml } from "./text-format.js";
|
|
6
|
+
import { withTimeout } from "../../core/agent/prompt-timeout.js";
|
|
6
7
|
import { normalizeArtifactForReasoning, shouldNormalizeArtifactToText } from "../../core/artifacts/normalize-for-reasoning.js";
|
|
7
8
|
|
|
9
|
+
const promptTimeoutMs = 300_000;
|
|
10
|
+
|
|
8
11
|
function quotedMessageSummary(message) {
|
|
9
12
|
if (!message) return [];
|
|
10
13
|
|
|
@@ -154,15 +157,52 @@ async function normalizeIncomingArtifact({ artifact, toolRegistry, chatArtifactS
|
|
|
154
157
|
return { transcript: normalizedArtifact, toolResult };
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
|
|
160
|
+
function sessionEventLogMessage(event) {
|
|
161
|
+
if (event.type === "tool_execution_start") {
|
|
162
|
+
return `tool ${event.toolName} started`;
|
|
163
|
+
}
|
|
164
|
+
if (event.type === "tool_execution_end") {
|
|
165
|
+
return `tool ${event.toolName} ${event.isError ? "failed" : "finished"}`;
|
|
166
|
+
}
|
|
167
|
+
if (event.type === "auto_retry_start") {
|
|
168
|
+
return `auto retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms: ${event.errorMessage}`;
|
|
169
|
+
}
|
|
170
|
+
if (event.type === "auto_retry_end") {
|
|
171
|
+
return event.success
|
|
172
|
+
? `auto retry succeeded after ${event.attempt} attempt(s)`
|
|
173
|
+
: `auto retry failed after ${event.attempt} attempt(s): ${event.finalError || "unknown error"}`;
|
|
174
|
+
}
|
|
175
|
+
if (event.type === "compaction_start") {
|
|
176
|
+
return `compaction started (${event.reason})`;
|
|
177
|
+
}
|
|
178
|
+
if (event.type === "compaction_end") {
|
|
179
|
+
return `compaction ${event.aborted ? "aborted" : "finished"} (${event.reason})`;
|
|
180
|
+
}
|
|
181
|
+
if (event.type === "message_end" && event.message?.stopReason === "error") {
|
|
182
|
+
return `assistant message ended with error: ${event.message.errorMessage || "unknown error"}`;
|
|
183
|
+
}
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function collectText(session, prompt, { logger, chatId } = {}) {
|
|
158
188
|
let text = "";
|
|
159
189
|
const unsubscribe = session.subscribe((event) => {
|
|
160
190
|
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
161
191
|
text += event.assistantMessageEvent.delta;
|
|
162
192
|
}
|
|
193
|
+
const logMessage = sessionEventLogMessage(event);
|
|
194
|
+
if (logMessage) logger?.log("agent", `chat ${chatId} ${logMessage}`);
|
|
163
195
|
});
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await withTimeout(session.prompt(prompt), {
|
|
199
|
+
timeoutMs: promptTimeoutMs,
|
|
200
|
+
label: `Telegram prompt for chat ${chatId}`
|
|
201
|
+
});
|
|
202
|
+
} finally {
|
|
203
|
+
unsubscribe();
|
|
204
|
+
}
|
|
205
|
+
|
|
166
206
|
return text.trim();
|
|
167
207
|
}
|
|
168
208
|
|
|
@@ -182,6 +222,7 @@ async function withTyping(ctx, work) {
|
|
|
182
222
|
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger, webhookUrl, setHttpRequestHandler }) {
|
|
183
223
|
const bot = new Bot(config.telegram.token);
|
|
184
224
|
const perChatState = new Map();
|
|
225
|
+
let taskTimer = null;
|
|
185
226
|
|
|
186
227
|
function getIncomingChatMeta(ctx) {
|
|
187
228
|
return {
|
|
@@ -252,7 +293,13 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
252
293
|
async function processPromptForChat({ chatId, prompt, ctx = null }) {
|
|
253
294
|
const work = async () => {
|
|
254
295
|
const { session } = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
|
|
255
|
-
|
|
296
|
+
let text = "";
|
|
297
|
+
try {
|
|
298
|
+
text = await collectText(session, prompt, { logger, chatId });
|
|
299
|
+
} catch (error) {
|
|
300
|
+
agentManager.resetSession(chatId);
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
256
303
|
if (text) {
|
|
257
304
|
await sendTextReply({
|
|
258
305
|
sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
|
|
@@ -283,12 +330,19 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
283
330
|
let currentPrompt = prompt;
|
|
284
331
|
let currentCtx = ctx;
|
|
285
332
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
333
|
+
try {
|
|
334
|
+
while (currentPrompt) {
|
|
335
|
+
try {
|
|
336
|
+
logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
|
|
337
|
+
await processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
|
|
338
|
+
} catch (error) {
|
|
339
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
340
|
+
logger?.error("telegram", `${label} failed for chat ${chatId}: ${message}`);
|
|
341
|
+
throw error;
|
|
342
|
+
} finally {
|
|
343
|
+
currentCtx = null;
|
|
344
|
+
}
|
|
345
|
+
|
|
292
346
|
if (chatState.nextPrompt) {
|
|
293
347
|
currentPrompt = chatState.nextPrompt;
|
|
294
348
|
chatState.nextPrompt = "";
|
|
@@ -296,9 +350,9 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
296
350
|
currentPrompt = "";
|
|
297
351
|
}
|
|
298
352
|
}
|
|
353
|
+
} finally {
|
|
354
|
+
chatState.processing = false;
|
|
299
355
|
}
|
|
300
|
-
|
|
301
|
-
chatState.processing = false;
|
|
302
356
|
}
|
|
303
357
|
|
|
304
358
|
async function enqueueOrProcess(ctx) {
|
|
@@ -460,11 +514,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
460
514
|
await bot.api.setMyCommands([
|
|
461
515
|
{ command: "new", description: "Start a new chat context" }
|
|
462
516
|
]);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
517
|
+
if (!taskTimer) {
|
|
518
|
+
taskTimer = setInterval(() => {
|
|
519
|
+
dispatchDueTasks().catch((error) => {
|
|
520
|
+
logger?.error("tasks", `dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
521
|
+
});
|
|
522
|
+
}, 1000);
|
|
523
|
+
taskTimer.unref();
|
|
524
|
+
}
|
|
468
525
|
if (webhookUrl && setHttpRequestHandler) {
|
|
469
526
|
const webhookPath = `/telegram-${config.telegram.token.slice(-8)}`;
|
|
470
527
|
const handleUpdate = webhookCallback(bot, "http", {
|
|
@@ -485,6 +542,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
485
542
|
logger?.log("telegram", "bot polling started");
|
|
486
543
|
await bot.start({ drop_pending_updates: true });
|
|
487
544
|
}
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
async stop() {
|
|
548
|
+
if (taskTimer) clearInterval(taskTimer);
|
|
549
|
+
taskTimer = null;
|
|
550
|
+
try {
|
|
551
|
+
bot.stop();
|
|
552
|
+
} catch {}
|
|
488
553
|
}
|
|
489
554
|
};
|
|
490
555
|
}
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import defaults from "./config.js";
|
|
4
4
|
import { loadToolConfig } from "../../src/core/tools/tool-config.js";
|
|
5
5
|
import { toolError, toolNeedsConfig, toolOk } from "../../src/core/tools/tool-result.js";
|
|
6
|
-
import { getToolConfigPath,
|
|
6
|
+
import { getChatToolTmpDir, getToolConfigPath, getToolTmpDir } from "../../src/runtime/paths.js";
|
|
7
7
|
|
|
8
8
|
const toolName = "openai-tts";
|
|
9
9
|
const config = await loadToolConfig(toolName, defaults);
|
|
@@ -49,7 +49,9 @@ async function run(requestFile) {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const outDir =
|
|
52
|
+
const outDir = request.chatId != null
|
|
53
|
+
? getChatToolTmpDir(request.chatId, toolName)
|
|
54
|
+
: getToolTmpDir(toolName);
|
|
53
55
|
await mkdir(outDir, { recursive: true });
|
|
54
56
|
const filePath = path.join(outDir, `speech-${Date.now()}.ogg`);
|
|
55
57
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
|
|
4
|
-
const instructionsPath = fileURLToPath(new URL("../../../AGENTS.md", import.meta.url));
|
|
5
|
-
let cachedInstructions = null;
|
|
6
|
-
|
|
7
|
-
export async function loadProjectInstructions() {
|
|
8
|
-
if (cachedInstructions !== null) return cachedInstructions;
|
|
9
|
-
cachedInstructions = await readFile(instructionsPath, "utf8");
|
|
10
|
-
return cachedInstructions;
|
|
11
|
-
}
|