arisa 4.3.4 → 5.0.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 +18 -17
- package/README.md +30 -9
- package/package.json +6 -2
- package/pnpm-workspace.yaml +1 -0
- package/src/core/agent/agent-manager.js +288 -29
- package/src/core/agent/auth-flow.js +12 -8
- package/src/core/agent/model-selection.js +54 -14
- package/src/core/agent/model-speed.js +59 -0
- package/src/core/config/config-defaults.js +56 -4
- package/src/core/config/config-store.js +5 -1
- package/src/core/conversation/conversation-history-store.js +142 -0
- package/src/core/tasks/task-store.js +16 -0
- package/src/core/tools/daemon-health.js +11 -2
- package/src/core/tools/daemon-processes.js +92 -2
- package/src/core/tools/daemon-runtime.js +4 -2
- package/src/core/tools/ipc-client.js +15 -3
- package/src/core/tools/tool-registry.js +27 -0
- package/src/index.js +61 -6
- package/src/runtime/arisa-capabilities.js +45 -1
- package/src/runtime/bootstrap.js +3 -2
- package/src/runtime/create-app.js +47 -11
- package/src/runtime/doctor.js +307 -0
- package/src/runtime/log-viewer.js +165 -0
- package/src/runtime/paths.js +4 -1
- package/src/runtime/service-manager.js +106 -8
- package/src/runtime/tool-process-supervisor.js +107 -10
- package/src/transport/telegram/bot.js +533 -99
- package/src/transport/telegram/model-picker.js +28 -2
- package/test/agent-tool-policy.test.js +26 -1
- package/test/auth-flow.test.js +28 -2
- package/test/capabilities-security.test.js +37 -0
- package/test/context-and-task-bounds.test.js +279 -0
- package/test/daemon-runtime.test.js +130 -2
- package/test/dependency-warnings.test.js +17 -0
- package/test/doctor.test.js +90 -0
- package/test/log-viewer.test.js +90 -0
- package/test/model-selection.test.js +125 -2
- package/test/paths.test.js +8 -0
- package/test/pi-compaction.test.js +43 -0
- package/test/service-manager.test.js +234 -0
- package/test/task-store.test.js +31 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { readFile, stat, unlink } from "node:fs/promises";
|
|
3
|
-
import { createAgentSession, DefaultResourceLoader, SessionManager, defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
|
|
6
6
|
import { resolveChatModelSelection } from "./model-selection.js";
|
|
@@ -9,6 +9,7 @@ import { withTimeout } from "./prompt-timeout.js";
|
|
|
9
9
|
import { buildPiToolPolicy, getCoreCodingTools } from "./core-tools.js";
|
|
10
10
|
import { createSystemShellTool } from "./system-shell-tool.js";
|
|
11
11
|
import { clampModelThinkingLevel } from "./pi-runtime.js";
|
|
12
|
+
import { clampModelSpeed, createModelSpeedController } from "./model-speed.js";
|
|
12
13
|
import { arisaHomeDir, getChatPiSessionsDir } from "../../runtime/paths.js";
|
|
13
14
|
|
|
14
15
|
const piValidationTimeoutMs = 60_000;
|
|
@@ -24,6 +25,113 @@ const arisaToolNames = [
|
|
|
24
25
|
"send_artifact"
|
|
25
26
|
];
|
|
26
27
|
|
|
28
|
+
function messageText(content) {
|
|
29
|
+
if (typeof content === "string") return content.trim();
|
|
30
|
+
if (!Array.isArray(content)) return "";
|
|
31
|
+
return content
|
|
32
|
+
.filter((item) => item?.type === "text" && typeof item.text === "string")
|
|
33
|
+
.map((item) => item.text.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatPortableSessionHistory(messages = []) {
|
|
39
|
+
return messages
|
|
40
|
+
.map((message) => {
|
|
41
|
+
const text = messageText(message?.content);
|
|
42
|
+
if (!text) return "";
|
|
43
|
+
const role = message.role === "assistant"
|
|
44
|
+
? "Assistant"
|
|
45
|
+
: message.role === "user"
|
|
46
|
+
? "User"
|
|
47
|
+
: (message.customType ? `Session memory (${message.customType})` : "Session context");
|
|
48
|
+
return `${role}:\n${text}`;
|
|
49
|
+
})
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join("\n\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const estimatedImageTokens = 1_200;
|
|
55
|
+
|
|
56
|
+
function estimateContentTokens(content) {
|
|
57
|
+
if (typeof content === "string") return Math.ceil(content.length / 4);
|
|
58
|
+
if (!Array.isArray(content)) return 0;
|
|
59
|
+
const chars = content.reduce((total, item) => {
|
|
60
|
+
if (item?.type === "image") return total + estimatedImageTokens * 4;
|
|
61
|
+
if (typeof item?.text === "string") return total + item.text.length;
|
|
62
|
+
if (typeof item?.thinking === "string") return total + item.thinking.length;
|
|
63
|
+
if (item?.type === "toolCall") {
|
|
64
|
+
return total + String(item.name || "").length + JSON.stringify(item.arguments || {}).length;
|
|
65
|
+
}
|
|
66
|
+
return total;
|
|
67
|
+
}, 0);
|
|
68
|
+
return Math.ceil(chars / 4);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function estimateMessageTokens(message) {
|
|
72
|
+
if (["user", "assistant", "custom", "toolResult"].includes(message?.role)) {
|
|
73
|
+
return estimateContentTokens(message.content);
|
|
74
|
+
}
|
|
75
|
+
if (message?.role === "bashExecution") {
|
|
76
|
+
return Math.ceil((String(message.command || "").length + String(message.output || "").length) / 4);
|
|
77
|
+
}
|
|
78
|
+
if (["branchSummary", "compactionSummary"].includes(message?.role)) {
|
|
79
|
+
return Math.ceil(String(message.summary || "").length / 4);
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function summarizeRetainedContext(messages = []) {
|
|
85
|
+
const sizes = messages.map((message) => ({
|
|
86
|
+
role: message?.role,
|
|
87
|
+
tokens: estimateMessageTokens(message)
|
|
88
|
+
}));
|
|
89
|
+
const estimatedTokens = sizes.reduce((total, item) => total + item.tokens, 0);
|
|
90
|
+
const toolResultTokens = sizes
|
|
91
|
+
.filter((item) => item.role === "toolResult")
|
|
92
|
+
.reduce((total, item) => total + item.tokens, 0);
|
|
93
|
+
const largestMessageTokens = sizes.reduce((largest, item) => Math.max(largest, item.tokens), 0);
|
|
94
|
+
return {
|
|
95
|
+
messages: messages.length,
|
|
96
|
+
estimatedTokens,
|
|
97
|
+
toolResultPercent: estimatedTokens ? toolResultTokens / estimatedTokens * 100 : 0,
|
|
98
|
+
largestMessagePercent: estimatedTokens ? largestMessageTokens / estimatedTokens * 100 : 0
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function closeAgentSession(session) {
|
|
103
|
+
if (session?.close) return session.close();
|
|
104
|
+
if (session?.dispose) return session.dispose();
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const defaultScheduledTaskListLimit = 50;
|
|
109
|
+
export const maxScheduledTaskListLimit = 100;
|
|
110
|
+
|
|
111
|
+
export function selectScheduledTasks(tasks = [], { status, limit = defaultScheduledTaskListLimit } = {}) {
|
|
112
|
+
const parsedLimit = Number(limit);
|
|
113
|
+
const resolvedLimit = Math.min(
|
|
114
|
+
Math.max(Number.isFinite(parsedLimit) ? Math.trunc(parsedLimit) : defaultScheduledTaskListLimit, 1),
|
|
115
|
+
maxScheduledTaskListLimit
|
|
116
|
+
);
|
|
117
|
+
const allTasks = Array.isArray(tasks) ? tasks : [];
|
|
118
|
+
const orderedTasks = status
|
|
119
|
+
? [...allTasks].reverse()
|
|
120
|
+
: [
|
|
121
|
+
...allTasks.filter((task) => task.status === "pending" || task.status === "running").reverse(),
|
|
122
|
+
...allTasks.filter((task) => task.status !== "pending" && task.status !== "running").reverse()
|
|
123
|
+
];
|
|
124
|
+
const visibleTasks = orderedTasks.slice(0, resolvedLimit);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
tasks: visibleTasks,
|
|
128
|
+
total: allTasks.length,
|
|
129
|
+
returned: visibleTasks.length,
|
|
130
|
+
limit: resolvedLimit,
|
|
131
|
+
truncated: visibleTasks.length < allTasks.length
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
27
135
|
function isLocalBaseUrl(value) {
|
|
28
136
|
if (typeof value !== "string" || !value.trim()) return false;
|
|
29
137
|
try {
|
|
@@ -90,11 +198,16 @@ async function assertDirectory(dir, label) {
|
|
|
90
198
|
}
|
|
91
199
|
}
|
|
92
200
|
|
|
93
|
-
|
|
201
|
+
export function createPiSettingsManager(config) {
|
|
202
|
+
return SettingsManager.inMemory({ compaction: { ...config.pi.compaction } });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function createArisaResourceLoader({ cwd, agentDir, settingsManager }) {
|
|
94
206
|
const arisaAgentsContent = await readFile(arisaAgentsFile, "utf8");
|
|
95
207
|
const resourceLoader = new DefaultResourceLoader({
|
|
96
208
|
cwd,
|
|
97
209
|
agentDir,
|
|
210
|
+
settingsManager,
|
|
98
211
|
agentsFilesOverride: (current) => appendArisaAgentsFile(current, arisaAgentsContent)
|
|
99
212
|
});
|
|
100
213
|
await resourceLoader.reload();
|
|
@@ -110,22 +223,104 @@ export class AgentManager {
|
|
|
110
223
|
this.logger = logger;
|
|
111
224
|
this.sessions = new Map();
|
|
112
225
|
this.pendingNewSessions = new Set();
|
|
226
|
+
this.pendingSessionHandoffs = new Map();
|
|
227
|
+
this.artifactDeliveryHandler = null;
|
|
228
|
+
this.sessionClosePromises = new Map();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
setArtifactDeliveryHandler(handler) {
|
|
232
|
+
this.artifactDeliveryHandler = handler;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async deliverArtifact(payload) {
|
|
236
|
+
if (!this.artifactDeliveryHandler) throw new Error("Telegram artifact delivery is unavailable");
|
|
237
|
+
return this.artifactDeliveryHandler(payload);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
closeCachedSession(sessionKey) {
|
|
241
|
+
const key = String(sessionKey);
|
|
242
|
+
const existing = this.sessions.get(key);
|
|
243
|
+
this.sessions.delete(key);
|
|
244
|
+
const closeSession = (existing?.session?.close || existing?.session?.dispose)
|
|
245
|
+
? () => closeAgentSession(existing.session)
|
|
246
|
+
: null;
|
|
247
|
+
if (!closeSession) {
|
|
248
|
+
return this.sessionClosePromises.get(key) || Promise.resolve();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const previousClose = this.sessionClosePromises.get(key);
|
|
252
|
+
const closePromise = Promise.resolve(previousClose)
|
|
253
|
+
.catch(() => {})
|
|
254
|
+
.then(closeSession)
|
|
255
|
+
.catch((error) => {
|
|
256
|
+
this.logger?.error?.("agent", `session close failed for chat ${key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
257
|
+
})
|
|
258
|
+
.finally(() => {
|
|
259
|
+
if (this.sessionClosePromises.get(key) === closePromise) {
|
|
260
|
+
this.sessionClosePromises.delete(key);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
this.sessionClosePromises.set(key, closePromise);
|
|
264
|
+
return closePromise;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async waitForSessionClose(sessionKey) {
|
|
268
|
+
const key = String(sessionKey);
|
|
269
|
+
let closing = this.sessionClosePromises.get(key);
|
|
270
|
+
while (closing) {
|
|
271
|
+
await closing;
|
|
272
|
+
closing = this.sessionClosePromises.get(key);
|
|
273
|
+
}
|
|
113
274
|
}
|
|
114
275
|
|
|
115
276
|
setConfig(config) {
|
|
277
|
+
for (const key of this.sessions.keys()) this.closeCachedSession(key);
|
|
116
278
|
this.config = config;
|
|
117
|
-
this.sessions.clear();
|
|
118
279
|
this.pendingNewSessions.clear();
|
|
280
|
+
this.pendingSessionHandoffs.clear();
|
|
119
281
|
}
|
|
120
282
|
|
|
121
|
-
resetSession(chatId) {
|
|
283
|
+
resetSession(chatId, { handoff = "", parentSession = "" } = {}) {
|
|
122
284
|
const sessionKey = String(chatId);
|
|
123
|
-
this.
|
|
285
|
+
this.closeCachedSession(sessionKey);
|
|
124
286
|
this.pendingNewSessions.add(sessionKey);
|
|
287
|
+
const text = String(handoff || "").trim();
|
|
288
|
+
const parent = String(parentSession || "").trim();
|
|
289
|
+
if (text || parent) {
|
|
290
|
+
this.pendingSessionHandoffs.set(sessionKey, { text, parentSession: parent });
|
|
291
|
+
} else {
|
|
292
|
+
this.pendingSessionHandoffs.delete(sessionKey);
|
|
293
|
+
}
|
|
125
294
|
}
|
|
126
295
|
|
|
127
296
|
clearSessionCache(chatId) {
|
|
128
|
-
this.
|
|
297
|
+
this.closeCachedSession(String(chatId));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async getRuntimeDiagnostic({ contextInspectionTimeoutMs } = {}) {
|
|
301
|
+
const contexts = await Promise.all([...this.sessions.entries()].map(async ([chatId, context]) => {
|
|
302
|
+
const base = { chatId };
|
|
303
|
+
try {
|
|
304
|
+
const stats = context.session.getSessionStats();
|
|
305
|
+
const retained = summarizeRetainedContext(context.session.messages);
|
|
306
|
+
return {
|
|
307
|
+
...base,
|
|
308
|
+
...retained,
|
|
309
|
+
tokens: stats.contextUsage?.tokens ?? null,
|
|
310
|
+
contextWindow: stats.contextUsage?.contextWindow ?? null,
|
|
311
|
+
percent: stats.contextUsage?.percent ?? null
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return { ...base, error: error instanceof Error ? error.message : String(error) };
|
|
315
|
+
}
|
|
316
|
+
}));
|
|
317
|
+
return {
|
|
318
|
+
harness: "pi",
|
|
319
|
+
sessions: this.sessions.size,
|
|
320
|
+
closingSessions: this.sessionClosePromises.size,
|
|
321
|
+
managedProcessIds: [],
|
|
322
|
+
contexts
|
|
323
|
+
};
|
|
129
324
|
}
|
|
130
325
|
|
|
131
326
|
createSessionManager(chatId, workspaceDir = arisaInstallDir, sessionRevision = 0) {
|
|
@@ -133,36 +328,60 @@ export class AgentManager {
|
|
|
133
328
|
const sessionDir = getChatPiSessionsDir(sessionKey, sessionRevision);
|
|
134
329
|
if (this.pendingNewSessions.has(sessionKey)) {
|
|
135
330
|
this.logger?.log("agent", `starting new persisted session for chat ${sessionKey}`);
|
|
136
|
-
|
|
331
|
+
const handoff = this.pendingSessionHandoffs.get(sessionKey);
|
|
332
|
+
const sessionManager = SessionManager.create(
|
|
333
|
+
workspaceDir,
|
|
334
|
+
sessionDir,
|
|
335
|
+
handoff?.parentSession ? { parentSession: handoff.parentSession } : undefined
|
|
336
|
+
);
|
|
337
|
+
if (handoff?.text) {
|
|
338
|
+
sessionManager.appendCustomMessageEntry(
|
|
339
|
+
"arisa-session-handoff",
|
|
340
|
+
handoff.text,
|
|
341
|
+
false,
|
|
342
|
+
{ source: "telegram-new" }
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return { sessionManager, isNewSession: true };
|
|
137
346
|
}
|
|
138
347
|
this.logger?.log("agent", `recovering persisted session for chat ${sessionKey}`);
|
|
139
348
|
return { sessionManager: SessionManager.continueRecent(workspaceDir, sessionDir), isNewSession: false };
|
|
140
349
|
}
|
|
141
350
|
|
|
142
|
-
async validatePiAgent() {
|
|
351
|
+
async validatePiAgent(config = this.config) {
|
|
143
352
|
this.logger?.log("agent", "validating Pi session");
|
|
144
353
|
const { authStorage, modelRegistry } = createPiRuntime({
|
|
145
|
-
provider:
|
|
146
|
-
apiKey:
|
|
354
|
+
provider: config.pi.provider,
|
|
355
|
+
apiKey: config.pi.apiKey
|
|
147
356
|
});
|
|
148
|
-
const model = modelRegistry.find(
|
|
357
|
+
const model = modelRegistry.find(config.pi.provider, config.pi.model);
|
|
149
358
|
if (!model) {
|
|
150
|
-
throw new Error(`Model not found: ${
|
|
359
|
+
throw new Error(`Model not found: ${config.pi.provider}/${config.pi.model}`);
|
|
151
360
|
}
|
|
152
|
-
if (requiresProviderAuth(model) && !
|
|
153
|
-
throw new Error(`No auth found for ${
|
|
361
|
+
if (requiresProviderAuth(model) && !config.pi.apiKey && !hasProviderAuth(config.pi.provider, { authStorage, modelRegistry })) {
|
|
362
|
+
throw new Error(`No auth found for ${config.pi.provider}. Provide a Pi API key in bootstrap, or authenticate with Pi login for this provider during bootstrap.`);
|
|
154
363
|
}
|
|
155
364
|
|
|
365
|
+
const settingsManager = createPiSettingsManager(config);
|
|
156
366
|
const { session } = await createAgentSession({
|
|
157
367
|
authStorage,
|
|
158
368
|
modelRegistry,
|
|
159
369
|
model,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
await withTimeout(promptAndThrowOnAssistantError(session, "Reply with exactly: OK"), {
|
|
163
|
-
timeoutMs: piValidationTimeoutMs,
|
|
164
|
-
label: "Pi validation prompt"
|
|
370
|
+
settingsManager,
|
|
371
|
+
sessionManager: SessionManager.inMemory()
|
|
165
372
|
});
|
|
373
|
+
try {
|
|
374
|
+
await withTimeout(promptAndThrowOnAssistantError(session, "Reply with exactly: OK"), {
|
|
375
|
+
timeoutMs: piValidationTimeoutMs,
|
|
376
|
+
label: "Pi validation prompt"
|
|
377
|
+
});
|
|
378
|
+
} finally {
|
|
379
|
+
session.dispose();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async validateAgent(config = this.config) {
|
|
384
|
+
return this.validatePiAgent(config);
|
|
166
385
|
}
|
|
167
386
|
|
|
168
387
|
async getSessionContext(chatId, telegram) {
|
|
@@ -178,11 +397,16 @@ export class AgentManager {
|
|
|
178
397
|
this.logger?.log("agent", `updating effort for chat ${sessionKey}: ${existing.session.thinkingLevel} -> ${desiredThinkingLevel}`);
|
|
179
398
|
existing.session.setThinkingLevel(desiredThinkingLevel);
|
|
180
399
|
}
|
|
400
|
+
const desiredSpeed = clampModelSpeed(existing.session.model, modelSelection.speed);
|
|
401
|
+
if (existing.speedController.speed !== desiredSpeed) {
|
|
402
|
+
this.logger?.log("agent", `updating speed for chat ${sessionKey}: ${existing.speedController.speed}x -> ${desiredSpeed}x`);
|
|
403
|
+
existing.speedController.setSpeed(desiredSpeed);
|
|
404
|
+
}
|
|
181
405
|
this.logger?.log("agent", `reusing session for chat ${sessionKey}`);
|
|
182
406
|
return existing;
|
|
183
407
|
}
|
|
184
408
|
this.logger?.log("agent", `model changed for chat ${sessionKey}: ${existing?.modelKey || "unknown"} -> ${effectiveModelKey}; recreating session`);
|
|
185
|
-
this.
|
|
409
|
+
this.closeCachedSession(sessionKey);
|
|
186
410
|
this.pendingNewSessions.add(sessionKey);
|
|
187
411
|
}
|
|
188
412
|
|
|
@@ -196,6 +420,7 @@ export class AgentManager {
|
|
|
196
420
|
throw new Error(`No auth found for ${this.config.pi.provider}. Re-run bootstrap and complete login for this provider before Telegram starts.`);
|
|
197
421
|
}
|
|
198
422
|
const thinkingLevel = clampModelThinkingLevel(model, modelSelection.thinkingLevel);
|
|
423
|
+
const speed = clampModelSpeed(model, modelSelection.speed);
|
|
199
424
|
|
|
200
425
|
const policy = buildPiToolPolicy({
|
|
201
426
|
config: this.config,
|
|
@@ -208,14 +433,16 @@ export class AgentManager {
|
|
|
208
433
|
modelSelection.sessionRevision
|
|
209
434
|
);
|
|
210
435
|
const hasExistingSession = sessionManager.buildSessionContext().messages.length > 0;
|
|
211
|
-
this.logger?.log("agent", `${hasExistingSession ? "resuming" : "creating"} session for chat ${sessionKey} with model ${effectiveModelId} effort ${thinkingLevel}`);
|
|
436
|
+
this.logger?.log("agent", `${hasExistingSession ? "resuming" : "creating"} session for chat ${sessionKey} with model ${effectiveModelId} effort ${thinkingLevel} speed ${speed}x`);
|
|
212
437
|
const customTools = [
|
|
213
438
|
...this.createTools(telegram, chatId, policy),
|
|
214
439
|
createSystemShellTool({ workspaceDir: policy.workspaceDir, shell: policy.shell })
|
|
215
440
|
];
|
|
441
|
+
const settingsManager = createPiSettingsManager(this.config);
|
|
216
442
|
const resourceLoader = await createArisaResourceLoader({
|
|
217
443
|
cwd: policy.workspaceDir,
|
|
218
|
-
agentDir: arisaHomeDir
|
|
444
|
+
agentDir: arisaHomeDir,
|
|
445
|
+
settingsManager
|
|
219
446
|
});
|
|
220
447
|
const { session } = await createAgentSession({
|
|
221
448
|
cwd: policy.workspaceDir,
|
|
@@ -228,8 +455,11 @@ export class AgentManager {
|
|
|
228
455
|
tools: policy.tools,
|
|
229
456
|
excludeTools: policy.excludeTools,
|
|
230
457
|
customTools,
|
|
458
|
+
settingsManager,
|
|
231
459
|
sessionManager
|
|
232
460
|
});
|
|
461
|
+
const speedController = createModelSpeedController(session.agent.streamFn, speed);
|
|
462
|
+
session.agent.streamFn = speedController.streamFn;
|
|
233
463
|
|
|
234
464
|
if (!hasExistingSession) {
|
|
235
465
|
this.logger?.log("agent", `created new session for chat ${sessionKey}`);
|
|
@@ -239,14 +469,38 @@ export class AgentManager {
|
|
|
239
469
|
})}`);
|
|
240
470
|
}
|
|
241
471
|
|
|
242
|
-
const ctx = { session, modelId: effectiveModelId, modelKey: effectiveModelKey };
|
|
472
|
+
const ctx = { session, modelId: effectiveModelId, modelKey: effectiveModelKey, speedController };
|
|
243
473
|
this.sessions.set(sessionKey, ctx);
|
|
244
474
|
if (isNewSession) {
|
|
245
475
|
this.pendingNewSessions.delete(sessionKey);
|
|
476
|
+
this.pendingSessionHandoffs.delete(sessionKey);
|
|
246
477
|
}
|
|
247
478
|
return ctx;
|
|
248
479
|
}
|
|
249
480
|
|
|
481
|
+
async getAvailableModels(chatId) {
|
|
482
|
+
const { listProviderModels } = await import("./pi-runtime.js");
|
|
483
|
+
const runtime = createPiRuntime({ provider: this.config.pi.provider, apiKey: this.config.pi.apiKey });
|
|
484
|
+
return listProviderModels(this.config.pi.provider, runtime);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async setModelSpeed(chatId, speed) {
|
|
488
|
+
const context = this.sessions.get(String(chatId));
|
|
489
|
+
if (!context) return speed;
|
|
490
|
+
const effectiveSpeed = clampModelSpeed(context.session.model, speed);
|
|
491
|
+
context.speedController.setSpeed(effectiveSpeed);
|
|
492
|
+
return effectiveSpeed;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async close() {
|
|
496
|
+
const contexts = [...this.sessions.values()];
|
|
497
|
+
this.sessions.clear();
|
|
498
|
+
await Promise.allSettled([
|
|
499
|
+
...this.sessionClosePromises.values(),
|
|
500
|
+
...contexts.map((context) => closeAgentSession(context.session))
|
|
501
|
+
]);
|
|
502
|
+
}
|
|
503
|
+
|
|
250
504
|
async runTool({ name, request, chatId }) {
|
|
251
505
|
await this.toolRegistry.load();
|
|
252
506
|
this.logger?.log("agent", `run_tool ${name}`);
|
|
@@ -297,7 +551,7 @@ export class AgentManager {
|
|
|
297
551
|
defineTool({
|
|
298
552
|
name: "list_tools",
|
|
299
553
|
label: "List tools",
|
|
300
|
-
description: "List Arisa core, native shell, and modular CLI tools with their capabilities.",
|
|
554
|
+
description: "List Arisa core, native shell, and modular CLI tools with their capabilities and daemon diagnostics.",
|
|
301
555
|
parameters: Type.Object({}),
|
|
302
556
|
execute: async () => {
|
|
303
557
|
await this.toolRegistry.load();
|
|
@@ -313,7 +567,7 @@ export class AgentManager {
|
|
|
313
567
|
shell: policy.shell.shellPath || (process.platform === "win32" ? "powershell" : "sh"),
|
|
314
568
|
enabled: !(policy.excludeTools || []).includes("system_shell")
|
|
315
569
|
}];
|
|
316
|
-
const cliTools = this.toolRegistry.
|
|
570
|
+
const cliTools = (await this.toolRegistry.listWithRuntime(chatId)).map((tool) => ({
|
|
317
571
|
...tool,
|
|
318
572
|
source: "arisa-modular",
|
|
319
573
|
invocation: "run_tool"
|
|
@@ -410,15 +664,20 @@ export class AgentManager {
|
|
|
410
664
|
defineTool({
|
|
411
665
|
name: "list_scheduled_tasks",
|
|
412
666
|
label: "List scheduled tasks",
|
|
413
|
-
description: "List scheduled async tasks for the current Telegram chat.",
|
|
667
|
+
description: "List scheduled async tasks for the current Telegram chat. Results default to 50 tasks, always include pending/running tasks, and accept an optional limit up to 100.",
|
|
414
668
|
parameters: Type.Object({
|
|
415
|
-
status: Type.Optional(Type.String())
|
|
669
|
+
status: Type.Optional(Type.String()),
|
|
670
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: maxScheduledTaskListLimit }))
|
|
416
671
|
}),
|
|
417
672
|
execute: async (_id, params) => {
|
|
418
673
|
const tasks = await this.taskStore.list({ chatId, status: params.status });
|
|
674
|
+
const result = selectScheduledTasks(tasks, {
|
|
675
|
+
status: params.status,
|
|
676
|
+
limit: params.limit
|
|
677
|
+
});
|
|
419
678
|
return {
|
|
420
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
421
|
-
details:
|
|
679
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
680
|
+
details: result
|
|
422
681
|
};
|
|
423
682
|
}
|
|
424
683
|
}),
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createPiRuntime, hasProviderAuth, supportsProviderOAuth } from "./pi-runtime.js";
|
|
2
|
+
import { resolveChatModelSelection } from "./model-selection.js";
|
|
2
3
|
|
|
3
4
|
const authInvalidatedPatterns = [
|
|
4
5
|
/authentication token has been invalidated/i,
|
|
5
6
|
/token (?:has been )?invalidated/i,
|
|
6
7
|
/try signing in again/i,
|
|
7
|
-
/auth(?:entication)? token (?:expired|revoked|invalid)/i
|
|
8
|
+
/auth(?:entication)? token (?:is |has been )?(?:expired|revoked|invalid)/i
|
|
8
9
|
];
|
|
9
10
|
|
|
10
11
|
const missingAuthPatterns = [
|
|
@@ -32,23 +33,26 @@ export function getPiAuthIssue(error) {
|
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export function getPiAuthStatus(config) {
|
|
36
|
+
export function getPiAuthStatus(config, chatId = null) {
|
|
36
37
|
const runtime = createPiRuntime({
|
|
37
38
|
provider: config.pi.provider,
|
|
38
39
|
apiKey: config.pi.apiKey
|
|
39
40
|
});
|
|
41
|
+
const modelSelection = chatId == null
|
|
42
|
+
? { provider: config.pi.provider, model: config.pi.model }
|
|
43
|
+
: resolveChatModelSelection(config, chatId);
|
|
40
44
|
|
|
41
45
|
return {
|
|
42
|
-
provider:
|
|
43
|
-
model:
|
|
46
|
+
provider: modelSelection.provider,
|
|
47
|
+
model: modelSelection.model,
|
|
44
48
|
hasApiKey: Boolean(config.pi.apiKey),
|
|
45
49
|
hasStoredAuth: hasProviderAuth(config.pi.provider, runtime),
|
|
46
50
|
supportsOAuth: supportsProviderOAuth(config.pi.provider, runtime)
|
|
47
51
|
};
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
export function buildPiAuthTelegramMessage({ config, issue = null, verified = false }) {
|
|
51
|
-
const status = getPiAuthStatus(config);
|
|
54
|
+
export function buildPiAuthTelegramMessage({ config, chatId = null, issue = null, verified = false }) {
|
|
55
|
+
const status = getPiAuthStatus(config, chatId);
|
|
52
56
|
let title = `Pi authentication status for ${status.provider}/${status.model}.`;
|
|
53
57
|
if (issue) {
|
|
54
58
|
title = `Pi authentication needs attention for ${status.provider}/${status.model}.`;
|
|
@@ -91,8 +95,8 @@ export function buildPiAuthTelegramMessage({ config, issue = null, verified = fa
|
|
|
91
95
|
return lines.join("\n");
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
export function buildPiAuthRecoveryBlockedMessage({ config, issue = null, renewalActive = false }) {
|
|
95
|
-
const status = getPiAuthStatus(config);
|
|
98
|
+
export function buildPiAuthRecoveryBlockedMessage({ config, chatId = null, issue = null, renewalActive = false }) {
|
|
99
|
+
const status = getPiAuthStatus(config, chatId);
|
|
96
100
|
const lines = [
|
|
97
101
|
`Pi authentication is not ready for ${status.provider}/${status.model}.`,
|
|
98
102
|
"I did not send your message to the agent."
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { normalizeModelSpeed } from "./model-speed.js";
|
|
2
|
+
|
|
1
3
|
function chatKey(chatId) {
|
|
2
4
|
return String(chatId);
|
|
3
5
|
}
|
|
4
6
|
|
|
7
|
+
export function getAgentConfig(config) {
|
|
8
|
+
return config.pi;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
function normalizeSessionRevision(sessionRevision) {
|
|
6
12
|
if (sessionRevision == null) return 0;
|
|
7
13
|
if (!Number.isSafeInteger(sessionRevision) || sessionRevision < 0) {
|
|
@@ -11,12 +17,14 @@ function normalizeSessionRevision(sessionRevision) {
|
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export function resolveChatModelSelection(config, chatId) {
|
|
14
|
-
const
|
|
15
|
-
|
|
20
|
+
const agentConfig = getAgentConfig(config);
|
|
21
|
+
const selection = agentConfig.chatModels?.[chatKey(chatId)];
|
|
22
|
+
if (!selection || selection.provider !== agentConfig.provider) {
|
|
16
23
|
return {
|
|
17
|
-
provider:
|
|
18
|
-
model:
|
|
19
|
-
thinkingLevel:
|
|
24
|
+
provider: agentConfig.provider,
|
|
25
|
+
model: agentConfig.model,
|
|
26
|
+
thinkingLevel: agentConfig.thinkingLevel,
|
|
27
|
+
...(agentConfig.speed !== undefined ? { speed: normalizeModelSpeed(agentConfig.speed) } : {}),
|
|
20
28
|
sessionRevision: 0
|
|
21
29
|
};
|
|
22
30
|
}
|
|
@@ -24,7 +32,10 @@ export function resolveChatModelSelection(config, chatId) {
|
|
|
24
32
|
return {
|
|
25
33
|
provider: selection.provider,
|
|
26
34
|
model: selection.model,
|
|
27
|
-
thinkingLevel: selection.thinkingLevel ??
|
|
35
|
+
thinkingLevel: selection.thinkingLevel ?? agentConfig.thinkingLevel,
|
|
36
|
+
...(agentConfig.speed !== undefined
|
|
37
|
+
? { speed: normalizeModelSpeed(selection.speed ?? agentConfig.speed) }
|
|
38
|
+
: {}),
|
|
28
39
|
sessionRevision
|
|
29
40
|
};
|
|
30
41
|
}
|
|
@@ -37,29 +48,58 @@ export function resolveChatThinkingLevel(config, chatId) {
|
|
|
37
48
|
return resolveChatModelSelection(config, chatId).thinkingLevel;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
export function
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
export function resolveChatSpeed(config, chatId) {
|
|
52
|
+
const speed = resolveChatModelSelection(config, chatId).speed;
|
|
53
|
+
if (speed === undefined) throw new Error("Model speed is not configured for the active runtime");
|
|
54
|
+
return speed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function selectChatModel(config, chatId, model, { thinkingLevel, speed } = {}) {
|
|
58
|
+
const agentConfig = getAgentConfig(config);
|
|
59
|
+
if (model.provider !== agentConfig.provider) {
|
|
60
|
+
throw new Error(`Cannot select model from provider ${model.provider}; active provider is ${agentConfig.provider}`);
|
|
43
61
|
}
|
|
44
|
-
|
|
62
|
+
agentConfig.chatModels ||= {};
|
|
45
63
|
const key = chatKey(chatId);
|
|
46
|
-
const sessionRevision = (
|
|
47
|
-
|
|
64
|
+
const sessionRevision = (agentConfig.chatModels[key]?.sessionRevision || 0) + 1;
|
|
65
|
+
agentConfig.chatModels[key] = {
|
|
48
66
|
provider: model.provider,
|
|
49
67
|
model: model.id,
|
|
50
68
|
thinkingLevel,
|
|
69
|
+
...(agentConfig.speed !== undefined
|
|
70
|
+
? { speed: normalizeModelSpeed(speed ?? resolveChatModelSelection(config, chatId).speed) }
|
|
71
|
+
: {}),
|
|
51
72
|
sessionRevision
|
|
52
73
|
};
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
export function selectChatThinkingLevel(config, chatId, thinkingLevel) {
|
|
56
|
-
|
|
77
|
+
const agentConfig = getAgentConfig(config);
|
|
78
|
+
agentConfig.chatModels ||= {};
|
|
57
79
|
const key = chatKey(chatId);
|
|
58
80
|
const current = resolveChatModelSelection(config, chatId);
|
|
59
|
-
|
|
81
|
+
agentConfig.chatModels[key] = {
|
|
60
82
|
provider: current.provider,
|
|
61
83
|
model: current.model,
|
|
62
84
|
thinkingLevel,
|
|
85
|
+
...(current.speed !== undefined ? { speed: current.speed } : {}),
|
|
86
|
+
sessionRevision: current.sessionRevision
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function selectChatSpeed(config, chatId, speed) {
|
|
91
|
+
const agentConfig = getAgentConfig(config);
|
|
92
|
+
if (agentConfig.speed === undefined) {
|
|
93
|
+
throw new Error("Model speed is not configured for the active runtime");
|
|
94
|
+
}
|
|
95
|
+
agentConfig.chatModels ||= {};
|
|
96
|
+
const key = chatKey(chatId);
|
|
97
|
+
const current = resolveChatModelSelection(config, chatId);
|
|
98
|
+
agentConfig.chatModels[key] = {
|
|
99
|
+
provider: current.provider,
|
|
100
|
+
model: current.model,
|
|
101
|
+
thinkingLevel: current.thinkingLevel,
|
|
102
|
+
speed: normalizeModelSpeed(speed),
|
|
63
103
|
sessionRevision: current.sessionRevision
|
|
64
104
|
};
|
|
65
105
|
}
|