arisa 5.1.13 → 5.1.49
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 +4 -0
- package/README.md +2 -0
- package/package.json +8 -10
- package/src/core/agent/agent-manager.js +132 -70
- package/src/core/agent/pi-runtime.js +0 -8
- package/src/core/agent/system-shell-tool.js +13 -2
- package/src/core/artifacts/artifact-store.js +17 -18
- package/src/core/config/config-defaults.js +2 -2
- package/src/core/conversation/session-seed-store.js +85 -0
- package/src/core/tools/ipc-client.js +0 -2
- package/src/core/tools/official-tool-installer.js +78 -6
- package/src/core/tools/tool-dependencies.js +99 -0
- package/src/core/tools/tool-output-materializer.js +41 -0
- package/src/core/tools/tool-registry.js +145 -28
- package/src/official-tools.lock.json +209 -5
- package/src/runtime/arisa-capabilities.js +12 -1
- package/src/runtime/create-app.js +1 -0
- package/src/runtime/doctor.js +72 -23
- package/src/runtime/headless-tool-executor.js +2 -32
- package/src/runtime/paths.js +7 -1
- package/src/runtime/restart-receipt.js +90 -0
- package/src/runtime/tool-usage-report.js +25 -10
- package/src/transport/telegram/bot.js +403 -1015
- package/src/transport/telegram/chat-queue.js +132 -0
- package/src/transport/telegram/media.js +2 -2
- package/src/transport/telegram/model-callback.js +211 -0
- package/src/transport/telegram/model-controls.js +164 -0
- package/src/transport/telegram/prompt-builders.js +372 -0
- package/src/transport/telegram/task-dispatcher.js +94 -0
- package/src/transport/telegram/update-command.js +1 -1
- package/src/transport/telegram/workspace-group.js +83 -0
- package/test/agent-tool-policy.test.js +7 -1
- package/test/capabilities-security.test.js +21 -0
- package/test/context-and-task-bounds.test.js +33 -5
- package/test/doctor.test.js +57 -4
- package/test/model-selection.test.js +47 -1
- package/test/official-tool-dependencies.test.js +25 -0
- package/test/official-tool-installer.test.js +37 -9
- package/test/paths.test.js +4 -4
- package/test/restart-receipt.test.js +39 -0
- package/test/session-start-operational-notes.test.js +47 -0
- package/test/telegram-prompt-builders.test.js +33 -0
- package/test/telegram-task-dispatcher.test.js +102 -0
- package/test/telegram-workspace-group.test.js +76 -0
- package/test/tool-dependencies.test.js +53 -0
- package/test/tool-registry-run.test.js +81 -1
- package/test/tool-usage.test.js +26 -4
- package/test/topic-initialization.test.js +66 -0
- package/src/core/conversation/conversation-history-store.js +0 -142
package/AGENTS.md
CHANGED
|
@@ -47,6 +47,7 @@ Each tool declares in `tool.manifest.json`:
|
|
|
47
47
|
- `category`: optional broad capability bucket for discovery
|
|
48
48
|
- `keywords`: optional intent tags for capability discovery
|
|
49
49
|
- `skillHints`: optional skills to apply when using or editing the tool
|
|
50
|
+
- `toolDependencies`: optional required Arisa tools mapped to exact or caret semantic versions; use this only for hard runtime dependencies
|
|
50
51
|
|
|
51
52
|
## Text encoding
|
|
52
53
|
All textual content generated or sent by Arisa or its tools must use UTF-8.
|
|
@@ -78,6 +79,8 @@ const result = await arisa.tools.run({
|
|
|
78
79
|
|
|
79
80
|
The IPC channel is a local socket under `~/.arisa/state`. Every request must include `toolName`; chat-scoped capabilities also require `chatId`. Exposed capabilities are explicit: tools (`list`, `help`, `skills`, `setConfig`, `setResourceNote`, `getResourceNote`, `run`), artifacts (`createText`, `listRecent`, `get`, `deliver`), tasks (`add`, `list`, `cancel`, `cancelAll`), agent events (`enqueueEvent`), and runtime paths (`getChatToolStateDir`, `getToolStateDir`, `getChatToolTmpDir`, `getToolTmpDir`, `getChatArtifactsDir`). Do not expose raw `agentManager`, `taskStore`, `artifactStore`, or `toolRegistry` access.
|
|
80
81
|
|
|
82
|
+
`agent.enqueueEvent` may include an optional `acknowledgement` of at most 500 characters. Telegram delivers it before the event enters agent reasoning, so explicit authorization receipts cannot be delayed behind follow-up work. Keep it deterministic, concise, and free of credentials; the event prompt must not repeat it.
|
|
83
|
+
|
|
81
84
|
## Tool resource notes
|
|
82
85
|
A tool resource may have one deterministic chat-scoped operational note of at most 200 characters, keyed by the exact pair `toolName:resourceId`. Notes live in the chat state file returned by `getChatToolResourceNotesFile(chatId)`. They are separate from semantic memory and contain no credentials.
|
|
83
86
|
|
|
@@ -130,6 +133,7 @@ When such a tool is built, implement it with the shared daemon runtime instead o
|
|
|
130
133
|
- expose normal CLI behavior through `run --request-file`; callers should not manage daemon internals
|
|
131
134
|
- use the runtime for `daemon.pid`, `daemon.log`, `status.json`, and `commands/*.request|processing|result.json`
|
|
132
135
|
- keep one daemon owner per tool/session and avoid opening a second client over the same resource
|
|
136
|
+
- daemons that receive unsolicited external traffic (webhooks, bridge imports, persistent sockets, or active watchers) must use `daemon.autoStart: true` and disable idle shutdown while ingress is expected; idle shutdown is only for capabilities that can be restarted by the request initiating their next use
|
|
133
137
|
- use `beforeStart` only for tool-specific cleanup such as stale browser locks, without deleting persistent session/model data
|
|
134
138
|
- keep daemon tools headless/server-safe by default when they are meant to run on VPS machines
|
|
135
139
|
- if the daemon exposes an HTTP server, keep that server inside the tool; Arisa core does not discover or mount tool routes
|
package/README.md
CHANGED
|
@@ -76,6 +76,8 @@ Each tool folder contains:
|
|
|
76
76
|
|
|
77
77
|
`tool.manifest.json` may also include optional `skillHints`; Arisa resolves installed skills and passes them into `run_tool` requests as `skills` guidance, not runtime dependencies.
|
|
78
78
|
|
|
79
|
+
A tool that requires another Arisa tool declares a versioned `toolDependencies` map, for example `{ "mcp-client": "^0.1.0" }`. Official installation resolves these dependencies first, rejects missing or cyclic lock entries, and validates installed versions. The registry blocks execution when a required tool is missing or incompatible, and `/doctor` reports the issue.
|
|
80
|
+
|
|
79
81
|
Each tool is isolated from the root project and from other tools.
|
|
80
82
|
That isolation is part of the architecture:
|
|
81
83
|
|
package/package.json
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arisa",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.49",
|
|
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
|
-
"test": "node --test"
|
|
14
|
-
},
|
|
15
10
|
"keywords": [
|
|
16
11
|
"telegram",
|
|
17
12
|
"pi-agent",
|
|
@@ -44,11 +39,14 @@
|
|
|
44
39
|
"url": "https://github.com/clasen/Arisa/issues"
|
|
45
40
|
},
|
|
46
41
|
"homepage": "https://arisa.sh",
|
|
47
|
-
"packageManager": "pnpm@11.3.0+sha512.2c403d6594527287672b1f7056343a1f7c3634036a67ffabfcc2b3d7595d843768f8787148d1b57cf7956c90606bbd192857c363af19e96d2d0ec9ec5741d215",
|
|
48
42
|
"dependencies": {
|
|
49
43
|
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
50
44
|
"@sinclair/typebox": "^0.34.41",
|
|
51
|
-
"grammy": "^1.42.0"
|
|
52
|
-
|
|
45
|
+
"grammy": "^1.42.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"start": "node src/index.js",
|
|
49
|
+
"bootstrap": "node src/index.js --bootstrap",
|
|
50
|
+
"test": "node --test"
|
|
53
51
|
}
|
|
54
|
-
}
|
|
52
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
4
|
import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Type } from "@sinclair/typebox";
|
|
5
6
|
import { createPiRuntime, hasProviderAuth } from "./pi-runtime.js";
|
|
@@ -10,9 +11,10 @@ import { buildPiToolPolicy, getCoreCodingTools } from "./core-tools.js";
|
|
|
10
11
|
import { createSystemShellTool } from "./system-shell-tool.js";
|
|
11
12
|
import { clampModelThinkingLevel } from "./pi-runtime.js";
|
|
12
13
|
import { clampModelSpeed, createModelSpeedController } from "./model-speed.js";
|
|
13
|
-
import { arisaHomeDir, getChatPiSessionsDir } from "../../runtime/paths.js";
|
|
14
|
+
import { arisaHomeDir, getChatPiSessionsDir, sessionStartOperationalNotesFile } from "../../runtime/paths.js";
|
|
14
15
|
import { searchOfficialToolCatalog } from "../tools/official-tool-catalog.js";
|
|
15
16
|
import { ToolResourceNoteStore } from "../tools/tool-resource-note-store.js";
|
|
17
|
+
import { materializeToolOutput } from "../tools/tool-output-materializer.js";
|
|
16
18
|
|
|
17
19
|
const piValidationTimeoutMs = 60_000;
|
|
18
20
|
const arisaToolNames = [
|
|
@@ -25,33 +27,38 @@ const arisaToolNames = [
|
|
|
25
27
|
"list_scheduled_tasks",
|
|
26
28
|
"cancel_scheduled_task",
|
|
27
29
|
"cancel_all_scheduled_tasks",
|
|
30
|
+
"create_telegram_topic",
|
|
31
|
+
"initialize_telegram_topic",
|
|
28
32
|
"send_artifact"
|
|
29
33
|
];
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const operationalNoteMaxChars = 220;
|
|
36
|
+
|
|
37
|
+
function normalizeOperationalNote(note) {
|
|
38
|
+
const text = typeof note === "string" ? note : note?.text;
|
|
39
|
+
const trimmed = String(text || "").replace(/\s+/g, " ").trim();
|
|
40
|
+
if (!trimmed) return "";
|
|
41
|
+
return trimmed.length <= operationalNoteMaxChars ? trimmed : `${trimmed.slice(0, operationalNoteMaxChars - 1).trim()}…`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadSessionStartOperationalNotes() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = readFileSync(sessionStartOperationalNotesFile, "utf8");
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
const notes = Array.isArray(parsed) ? parsed : parsed?.notes;
|
|
49
|
+
if (!Array.isArray(notes)) return [];
|
|
50
|
+
return notes.map(normalizeOperationalNote).filter(Boolean).slice(0, 20);
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
? "Assistant"
|
|
48
|
-
: message.role === "user"
|
|
49
|
-
? "User"
|
|
50
|
-
: (message.customType ? `Session memory (${message.customType})` : "Session context");
|
|
51
|
-
return `${role}:\n${text}`;
|
|
52
|
-
})
|
|
53
|
-
.filter(Boolean)
|
|
54
|
-
.join("\n\n");
|
|
56
|
+
function formatSessionStartOperationalNotes(notes) {
|
|
57
|
+
if (!notes.length) return "";
|
|
58
|
+
return [
|
|
59
|
+
"Durable operating notes for this Arisa session:",
|
|
60
|
+
...notes.map((note) => `- ${note}`)
|
|
61
|
+
].join("\n");
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
const estimatedImageTokens = 1_200;
|
|
@@ -102,6 +109,16 @@ function summarizeRetainedContext(messages = []) {
|
|
|
102
109
|
};
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
function guardTools(tools, accessGuard) {
|
|
113
|
+
return tools.map((tool) => ({
|
|
114
|
+
...tool,
|
|
115
|
+
execute: async (...args) => {
|
|
116
|
+
await accessGuard();
|
|
117
|
+
return tool.execute(...args);
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
function closeAgentSession(session) {
|
|
106
123
|
if (session?.close) return session.close();
|
|
107
124
|
if (session?.dispose) return session.dispose();
|
|
@@ -301,7 +318,7 @@ export class AgentManager {
|
|
|
301
318
|
this.closeCachedSession(String(chatId));
|
|
302
319
|
}
|
|
303
320
|
|
|
304
|
-
async getRuntimeDiagnostic(
|
|
321
|
+
async getRuntimeDiagnostic() {
|
|
305
322
|
const contexts = await Promise.all([...this.sessions.entries()].map(async ([chatId, context]) => {
|
|
306
323
|
const base = { chatId };
|
|
307
324
|
try {
|
|
@@ -322,7 +339,6 @@ export class AgentManager {
|
|
|
322
339
|
harness: "pi",
|
|
323
340
|
sessions: this.sessions.size,
|
|
324
341
|
closingSessions: this.sessionClosePromises.size,
|
|
325
|
-
managedProcessIds: [],
|
|
326
342
|
contexts
|
|
327
343
|
};
|
|
328
344
|
}
|
|
@@ -338,6 +354,15 @@ export class AgentManager {
|
|
|
338
354
|
sessionDir,
|
|
339
355
|
handoff?.parentSession ? { parentSession: handoff.parentSession } : undefined
|
|
340
356
|
);
|
|
357
|
+
const operationalNotes = formatSessionStartOperationalNotes(loadSessionStartOperationalNotes());
|
|
358
|
+
if (operationalNotes) {
|
|
359
|
+
sessionManager.appendCustomMessageEntry(
|
|
360
|
+
"arisa-operational-notes",
|
|
361
|
+
operationalNotes,
|
|
362
|
+
false,
|
|
363
|
+
{ source: "session-start" }
|
|
364
|
+
);
|
|
365
|
+
}
|
|
341
366
|
if (handoff?.text) {
|
|
342
367
|
sessionManager.appendCustomMessageEntry(
|
|
343
368
|
"arisa-session-handoff",
|
|
@@ -388,7 +413,7 @@ export class AgentManager {
|
|
|
388
413
|
return this.validatePiAgent(config);
|
|
389
414
|
}
|
|
390
415
|
|
|
391
|
-
async getSessionContext(chatId, telegram) {
|
|
416
|
+
async getSessionContext(chatId, telegram, { scopeChatId = chatId, accessGuard = async () => {} } = {}) {
|
|
392
417
|
const sessionKey = String(chatId);
|
|
393
418
|
const modelSelection = resolveChatModelSelection(this.config, sessionKey);
|
|
394
419
|
const effectiveModelId = modelSelection.model;
|
|
@@ -406,6 +431,8 @@ export class AgentManager {
|
|
|
406
431
|
this.logger?.log("agent", `updating speed for chat ${sessionKey}: ${existing.speedController.speed}x -> ${desiredSpeed}x`);
|
|
407
432
|
existing.speedController.setSpeed(desiredSpeed);
|
|
408
433
|
}
|
|
434
|
+
existing.telegramTarget.current = telegram;
|
|
435
|
+
existing.accessGuardTarget.current = accessGuard;
|
|
409
436
|
this.logger?.log("agent", `reusing session for chat ${sessionKey}`);
|
|
410
437
|
return existing;
|
|
411
438
|
}
|
|
@@ -438,10 +465,26 @@ export class AgentManager {
|
|
|
438
465
|
);
|
|
439
466
|
const hasExistingSession = sessionManager.buildSessionContext().messages.length > 0;
|
|
440
467
|
this.logger?.log("agent", `${hasExistingSession ? "resuming" : "creating"} session for chat ${sessionKey} with model ${effectiveModelId} effort ${thinkingLevel} speed ${speed}x`);
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
468
|
+
const telegramTarget = { current: telegram };
|
|
469
|
+
const accessGuardTarget = { current: accessGuard };
|
|
470
|
+
const telegramProxy = {
|
|
471
|
+
sendMedia: (...args) => telegramTarget.current.sendMedia(...args),
|
|
472
|
+
createForumTopic: (...args) => telegramTarget.current.createForumTopic(...args),
|
|
473
|
+
initializeForumTopic: (...args) => telegramTarget.current.initializeForumTopic(...args),
|
|
474
|
+
prepareRestartReceipt: (...args) => telegramTarget.current.prepareRestartReceipt(...args),
|
|
475
|
+
cancelRestartReceipt: (...args) => telegramTarget.current.cancelRestartReceipt(...args),
|
|
476
|
+
getTaskContext: (...args) => telegramTarget.current.getTaskContext?.(...args) || null
|
|
477
|
+
};
|
|
478
|
+
const assertAccess = () => accessGuardTarget.current();
|
|
479
|
+
const customTools = guardTools([
|
|
480
|
+
...this.createTools(telegramProxy, scopeChatId, policy),
|
|
481
|
+
createSystemShellTool({
|
|
482
|
+
workspaceDir: policy.workspaceDir,
|
|
483
|
+
shell: policy.shell,
|
|
484
|
+
beforeRestart: (summary) => telegramProxy.prepareRestartReceipt(summary),
|
|
485
|
+
cancelRestart: (receiptId) => telegramProxy.cancelRestartReceipt(receiptId)
|
|
486
|
+
})
|
|
487
|
+
], assertAccess);
|
|
445
488
|
const settingsManager = createPiSettingsManager(this.config);
|
|
446
489
|
const resourceLoader = await createArisaResourceLoader({
|
|
447
490
|
cwd: policy.workspaceDir,
|
|
@@ -473,7 +516,14 @@ export class AgentManager {
|
|
|
473
516
|
})}`);
|
|
474
517
|
}
|
|
475
518
|
|
|
476
|
-
const ctx = {
|
|
519
|
+
const ctx = {
|
|
520
|
+
session,
|
|
521
|
+
modelId: effectiveModelId,
|
|
522
|
+
modelKey: effectiveModelKey,
|
|
523
|
+
speedController,
|
|
524
|
+
telegramTarget,
|
|
525
|
+
accessGuardTarget
|
|
526
|
+
};
|
|
477
527
|
this.sessions.set(sessionKey, ctx);
|
|
478
528
|
if (isNewSession) {
|
|
479
529
|
this.pendingNewSessions.delete(sessionKey);
|
|
@@ -505,10 +555,9 @@ export class AgentManager {
|
|
|
505
555
|
]);
|
|
506
556
|
}
|
|
507
557
|
|
|
508
|
-
async runTool({ name, request, chatId }) {
|
|
558
|
+
async runTool({ name, request, chatId, taskContext = null }) {
|
|
509
559
|
await this.toolRegistry.load();
|
|
510
560
|
this.logger?.log("agent", `run_tool ${name}`);
|
|
511
|
-
const chatArtifactStore = this.artifactStore.forChat(chatId);
|
|
512
561
|
const resourceId = String(request?.resourceId || "").trim();
|
|
513
562
|
const resourceNote = resourceId
|
|
514
563
|
? await this.resourceNotes.get(chatId, name, resourceId)
|
|
@@ -516,41 +565,14 @@ export class AgentManager {
|
|
|
516
565
|
const enrichedRequest = resourceNote ? { ...request, resourceId, resourceNote } : request;
|
|
517
566
|
const result = await this.toolRegistry.run({ name, request: enrichedRequest, chatId });
|
|
518
567
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (result.output?.filePath) {
|
|
529
|
-
const generated = await chatArtifactStore.createFromFile({
|
|
530
|
-
originalPath: result.output.filePath,
|
|
531
|
-
fileName: result.output.fileName || path.basename(result.output.filePath),
|
|
532
|
-
kind: result.output.kind || "file",
|
|
533
|
-
mimeType: result.output.mimeType || "application/octet-stream",
|
|
534
|
-
source: { type: "tool", toolName: name },
|
|
535
|
-
metadata: { tool: name, delivery: result.output.delivery }
|
|
536
|
-
});
|
|
537
|
-
result.output.artifactId = generated.id;
|
|
538
|
-
await unlink(result.output.filePath).catch(() => {});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (result.asyncTask || result.asyncTasks?.length) {
|
|
542
|
-
const scheduled = await this.taskStore.addMany(
|
|
543
|
-
result.asyncTasks || [result.asyncTask],
|
|
544
|
-
{
|
|
545
|
-
payload: { chatId },
|
|
546
|
-
source: { type: "tool", toolName: name, chatId }
|
|
547
|
-
}
|
|
548
|
-
);
|
|
549
|
-
result.asyncTasks = scheduled;
|
|
550
|
-
delete result.asyncTask;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return result;
|
|
568
|
+
return materializeToolOutput({
|
|
569
|
+
result,
|
|
570
|
+
name,
|
|
571
|
+
chatId,
|
|
572
|
+
artifactStore: this.artifactStore,
|
|
573
|
+
taskStore: this.taskStore,
|
|
574
|
+
taskContext
|
|
575
|
+
});
|
|
554
576
|
}
|
|
555
577
|
|
|
556
578
|
createTools(telegram, chatId, policy = buildPiToolPolicy({ config: this.config, customToolNames: arisaToolNames })) {
|
|
@@ -688,7 +710,8 @@ export class AgentManager {
|
|
|
688
710
|
resourceId: params.resourceId,
|
|
689
711
|
args: params.args || {}
|
|
690
712
|
},
|
|
691
|
-
chatId
|
|
713
|
+
chatId,
|
|
714
|
+
taskContext: telegram.getTaskContext()
|
|
692
715
|
});
|
|
693
716
|
|
|
694
717
|
if (params.deliver && result.output?.artifactId) {
|
|
@@ -757,6 +780,45 @@ export class AgentManager {
|
|
|
757
780
|
};
|
|
758
781
|
}
|
|
759
782
|
}),
|
|
783
|
+
defineTool({
|
|
784
|
+
name: "create_telegram_topic",
|
|
785
|
+
label: "Create Telegram topic",
|
|
786
|
+
description: "Create and initialize a new topic in the current owner-only Telegram forum. Topic names are dynamic, and context seeds the isolated session without copying unrelated history.",
|
|
787
|
+
parameters: Type.Object({
|
|
788
|
+
name: Type.String({ minLength: 1, maxLength: 128 }),
|
|
789
|
+
context: Type.String({ minLength: 1, maxLength: 4000 })
|
|
790
|
+
}),
|
|
791
|
+
execute: async (_id, params) => {
|
|
792
|
+
if (typeof telegram.createForumTopic !== "function") {
|
|
793
|
+
const result = { ok: false, error: "Telegram topic creation is unavailable in this chat." };
|
|
794
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
795
|
+
}
|
|
796
|
+
const result = await telegram.createForumTopic(params.name.trim(), params.context.trim());
|
|
797
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
|
|
798
|
+
}
|
|
799
|
+
}),
|
|
800
|
+
defineTool({
|
|
801
|
+
name: "initialize_telegram_topic",
|
|
802
|
+
label: "Initialize Telegram topic",
|
|
803
|
+
description: "Seed or replace the isolated context of an existing topic in the current owner-only Telegram forum.",
|
|
804
|
+
parameters: Type.Object({
|
|
805
|
+
messageThreadId: Type.Integer({ minimum: 2 }),
|
|
806
|
+
name: Type.String({ minLength: 1, maxLength: 128 }),
|
|
807
|
+
context: Type.String({ minLength: 1, maxLength: 4000 })
|
|
808
|
+
}),
|
|
809
|
+
execute: async (_id, params) => {
|
|
810
|
+
if (typeof telegram.initializeForumTopic !== "function") {
|
|
811
|
+
const result = { ok: false, error: "Telegram topic initialization is unavailable in this chat." };
|
|
812
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
813
|
+
}
|
|
814
|
+
const result = await telegram.initializeForumTopic({
|
|
815
|
+
messageThreadId: params.messageThreadId,
|
|
816
|
+
name: params.name.trim(),
|
|
817
|
+
context: params.context.trim()
|
|
818
|
+
});
|
|
819
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
|
|
820
|
+
}
|
|
821
|
+
}),
|
|
760
822
|
defineTool({
|
|
761
823
|
name: "send_artifact",
|
|
762
824
|
label: "Send artifact",
|
|
@@ -101,11 +101,3 @@ export function clampModelThinkingLevel(model, level) {
|
|
|
101
101
|
export function modelSupportsThinking(model) {
|
|
102
102
|
return listModelThinkingLevels(model).some((level) => level !== "off");
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
export function findPiModel({ provider, model, apiKey } = {}) {
|
|
106
|
-
const runtime = createPiRuntime({ provider, apiKey });
|
|
107
|
-
return {
|
|
108
|
-
...runtime,
|
|
109
|
-
model: provider && model ? runtime.modelRegistry.find(provider, model) : null
|
|
110
|
-
};
|
|
111
|
-
}
|
|
@@ -164,25 +164,36 @@ async function runShellCommand({ command, cwd, shellPath, timeoutMs }) {
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
export function
|
|
167
|
+
export function isArisaRestartCommand(command) {
|
|
168
|
+
return /(?:^|[;&|]\s*)arisa\s+restart(?:\s*(?:$|[;&|]))/i.test(String(command || ""));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function createSystemShellTool({ workspaceDir, shell = {}, beforeRestart, cancelRestart }) {
|
|
168
172
|
return defineTool({
|
|
169
173
|
name: "system_shell",
|
|
170
174
|
label: "System Shell",
|
|
171
175
|
description: "Run a command in the active Arisa workspace using the native system shell: PowerShell on Windows, and sh/bash-compatible shell on Unix.",
|
|
172
176
|
parameters: Type.Object({
|
|
173
177
|
command: Type.String({ description: "Command to execute in the active workspace." }),
|
|
174
|
-
timeoutMs: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds for this command." }))
|
|
178
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds for this command." })),
|
|
179
|
+
restartSummary: Type.Optional(Type.String({ maxLength: 500, description: "Concrete user-facing result to report after an arisa restart succeeds." }))
|
|
175
180
|
}),
|
|
176
181
|
execute: async (_id, params) => {
|
|
177
182
|
const timeoutMs = Number.isFinite(Number(params.timeoutMs)) && Number(params.timeoutMs) > 0
|
|
178
183
|
? Math.floor(Number(params.timeoutMs))
|
|
179
184
|
: (shell.timeoutMs || defaultTimeoutMs);
|
|
185
|
+
const restartReceipt = isArisaRestartCommand(params.command) && typeof beforeRestart === "function"
|
|
186
|
+
? await beforeRestart(params.restartSummary)
|
|
187
|
+
: null;
|
|
180
188
|
const result = await runShellCommand({
|
|
181
189
|
command: params.command,
|
|
182
190
|
cwd: workspaceDir,
|
|
183
191
|
shellPath: shell.shellPath,
|
|
184
192
|
timeoutMs
|
|
185
193
|
});
|
|
194
|
+
if (!result.ok && restartReceipt?.id && typeof cancelRestart === "function") {
|
|
195
|
+
await cancelRestart(restartReceipt.id).catch(() => {});
|
|
196
|
+
}
|
|
186
197
|
const details = {
|
|
187
198
|
stdout: result.stdout,
|
|
188
199
|
stderr: result.stderr,
|
|
@@ -86,14 +86,14 @@ class ChatArtifactStore {
|
|
|
86
86
|
return artifact;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
async
|
|
89
|
+
async createFileArtifact({ fileName, kind, mimeType, source, metadata = {}, writeFileContent }) {
|
|
90
90
|
await this.init();
|
|
91
91
|
await this.reload();
|
|
92
92
|
const artifactId = id();
|
|
93
93
|
const dir = path.join(this.rootDir, artifactId);
|
|
94
94
|
await mkdir(dir, { recursive: true });
|
|
95
95
|
const destPath = path.join(dir, fileName);
|
|
96
|
-
await
|
|
96
|
+
await writeFileContent(destPath);
|
|
97
97
|
const artifact = {
|
|
98
98
|
id: artifactId,
|
|
99
99
|
chatId: this.chatId,
|
|
@@ -109,27 +109,26 @@ class ChatArtifactStore {
|
|
|
109
109
|
return artifact;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
async createFromFile({ originalPath, fileName, kind, mimeType, source, metadata = {} }) {
|
|
113
|
+
return this.createFileArtifact({
|
|
114
|
+
fileName,
|
|
115
|
+
kind,
|
|
116
|
+
mimeType,
|
|
117
|
+
source,
|
|
118
|
+
metadata,
|
|
119
|
+
writeFileContent: (destPath) => copyArtifactFile(originalPath, destPath, mimeType)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
112
123
|
async createGeneratedFile({ fileName, content, kind, mimeType, source, metadata = {} }) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const artifactId = id();
|
|
116
|
-
const dir = path.join(this.rootDir, artifactId);
|
|
117
|
-
await mkdir(dir, { recursive: true });
|
|
118
|
-
const destPath = path.join(dir, fileName);
|
|
119
|
-
await writeArtifactFile(destPath, content);
|
|
120
|
-
const artifact = {
|
|
121
|
-
id: artifactId,
|
|
122
|
-
chatId: this.chatId,
|
|
124
|
+
return this.createFileArtifact({
|
|
125
|
+
fileName,
|
|
123
126
|
kind,
|
|
124
127
|
mimeType,
|
|
125
|
-
path: destPath,
|
|
126
128
|
source,
|
|
127
129
|
metadata,
|
|
128
|
-
|
|
129
|
-
};
|
|
130
|
-
this.items.push(artifact);
|
|
131
|
-
await this.saveIndex();
|
|
132
|
-
return artifact;
|
|
130
|
+
writeFileContent: (destPath) => writeArtifactFile(destPath, content)
|
|
131
|
+
});
|
|
133
132
|
}
|
|
134
133
|
|
|
135
134
|
async get(artifactId) {
|
|
@@ -18,11 +18,11 @@ export const daemonConfigDefaults = Object.freeze({
|
|
|
18
18
|
|
|
19
19
|
export const telegramConfigDefaults = Object.freeze({
|
|
20
20
|
modelPickerPageSize: 8,
|
|
21
|
-
busyMessageMode: "steer"
|
|
21
|
+
busyMessageMode: "steer",
|
|
22
|
+
ownerWorkspaceGroups: Object.freeze({})
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
export const doctorConfigDefaults = Object.freeze({
|
|
25
|
-
contextInspectionTimeoutMs: 5_000,
|
|
26
26
|
contextWarningPercent: 70,
|
|
27
27
|
contextCriticalPercent: 90,
|
|
28
28
|
contextInefficientMinTokens: 32_000,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
getChatLegacyConversationHistoryFile,
|
|
5
|
+
getChatSessionSeedFile
|
|
6
|
+
} from "../../runtime/paths.js";
|
|
7
|
+
|
|
8
|
+
function parseRecords(contents) {
|
|
9
|
+
return String(contents || "")
|
|
10
|
+
.replace(/^\uFEFF/, "")
|
|
11
|
+
.split(/\r?\n/)
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((line) => JSON.parse(line));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function seedText(record) {
|
|
17
|
+
return String(record?.context || record?.history || "").trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SessionSeedStore {
|
|
21
|
+
constructor({
|
|
22
|
+
seedFile = getChatSessionSeedFile,
|
|
23
|
+
legacyFile = getChatLegacyConversationHistoryFile
|
|
24
|
+
} = {}) {
|
|
25
|
+
this.seedFile = seedFile;
|
|
26
|
+
this.legacyFile = legacyFile;
|
|
27
|
+
this.locks = new Map();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async withChatLock(chatId, work) {
|
|
31
|
+
const key = String(chatId);
|
|
32
|
+
const previous = this.locks.get(key) || Promise.resolve();
|
|
33
|
+
const current = previous.catch(() => {}).then(work);
|
|
34
|
+
this.locks.set(key, current);
|
|
35
|
+
try {
|
|
36
|
+
return await current;
|
|
37
|
+
} finally {
|
|
38
|
+
if (this.locks.get(key) === current) this.locks.delete(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async write(chatId, record = null) {
|
|
43
|
+
const file = this.seedFile(chatId);
|
|
44
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
45
|
+
const body = record ? `${JSON.stringify(record)}\n` : "";
|
|
46
|
+
await writeFile(file, body, { encoding: "utf8", mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async set(chatId, context) {
|
|
50
|
+
const normalized = String(context || "").trim();
|
|
51
|
+
return this.withChatLock(chatId, async () => {
|
|
52
|
+
await this.write(chatId, normalized ? { kind: "seed", context: normalized } : null);
|
|
53
|
+
return Boolean(normalized);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async clear(chatId) {
|
|
58
|
+
return this.withChatLock(chatId, () => this.write(chatId));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async readPendingRecords(chatId) {
|
|
62
|
+
try {
|
|
63
|
+
return parseRecords(await readFile(this.seedFile(chatId), "utf8"));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error?.code !== "ENOENT") throw error;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return parseRecords(await readFile(this.legacyFile(chatId), "utf8"));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error?.code === "ENOENT") return [];
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async consume(chatId) {
|
|
76
|
+
return this.withChatLock(chatId, async () => {
|
|
77
|
+
const records = await this.readPendingRecords(chatId);
|
|
78
|
+
const pendingSeed = records.length === 1 && records[0]?.kind === "seed"
|
|
79
|
+
? seedText(records[0])
|
|
80
|
+
: "";
|
|
81
|
+
await this.write(chatId);
|
|
82
|
+
return pendingSeed;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -56,7 +56,6 @@ function requestIpc({ socketPath, request, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
|
56
56
|
export function createArisaClient({
|
|
57
57
|
toolName,
|
|
58
58
|
chatId = null,
|
|
59
|
-
capabilityToken = process.env.ARISA_IPC_TOKEN || "",
|
|
60
59
|
socketPath = process.env.ARISA_IPC_SOCKET || arisaIpcSocketFile
|
|
61
60
|
} = {}) {
|
|
62
61
|
if (typeof toolName !== "string" || !toolName.trim()) {
|
|
@@ -71,7 +70,6 @@ export function createArisaClient({
|
|
|
71
70
|
method,
|
|
72
71
|
toolName,
|
|
73
72
|
chatId,
|
|
74
|
-
capabilityToken,
|
|
75
73
|
params
|
|
76
74
|
}
|
|
77
75
|
});
|