arisa 5.1.49 → 5.1.64
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 +0 -2
- package/README.md +9 -0
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +49 -489
- package/src/core/agent/agent-session-lifecycle.js +181 -0
- package/src/core/agent/pi-capability-tools.js +183 -0
- package/src/core/artifacts/artifact-store.js +73 -17
- package/src/core/capabilities/capability-service.js +340 -0
- package/src/core/config/config-defaults.js +28 -1
- package/src/core/tasks/task-routing.js +7 -0
- package/src/core/tasks/task-runner.js +68 -0
- package/src/core/tasks/task-store.js +382 -92
- package/src/core/tools/tool-output-materializer.js +5 -5
- package/src/core/tools/tool-registry.js +20 -5
- package/src/core/tools/weighted-resource-governor.js +153 -0
- package/src/index.js +20 -0
- package/src/official-tools.lock.json +62 -45
- package/src/runtime/arisa-capabilities.js +51 -242
- package/src/runtime/create-app.js +11 -2
- package/src/runtime/create-headless-app.js +7 -4
- package/src/runtime/paths.js +4 -0
- package/src/runtime/service-manager.js +3 -1
- package/src/runtime/service-supervisor.js +98 -0
- package/src/transport/telegram/bot.js +186 -374
- package/src/transport/telegram/chat-queue.js +83 -6
- package/src/transport/telegram/prompt-builders.js +9 -0
- package/src/transport/telegram/reply-topic-routing.js +111 -0
- package/src/transport/telegram/task-dispatcher.js +96 -36
- package/src/transport/telegram/telegram-auth-controller.js +180 -0
- package/src/transport/telegram/telegram-session-bridge.js +177 -0
- package/src/transport/telegram/telegram-tools-command.js +28 -0
- package/src/transport/telegram/telegram-workspace-controller.js +66 -0
- package/src/transport/telegram/workspace-topic-store.js +228 -0
- package/test/agent-session-lifecycle.test.js +58 -0
- package/test/artifact-store.test.js +38 -2
- package/test/capabilities-security.test.js +58 -0
- package/test/chat-queue.test.js +32 -0
- package/test/context-and-task-bounds.test.js +76 -1
- package/test/device-code-message.test.js +9 -0
- package/test/media-caption.test.js +1 -1
- package/test/model-selection.test.js +9 -1
- package/test/official-tool-dependencies.test.js +1 -1
- package/test/paths.test.js +8 -0
- package/test/pi-capability-tools.test.js +65 -0
- package/test/service-manager.test.js +48 -0
- package/test/session-start-operational-notes.test.js +1 -1
- package/test/task-idempotency.test.js +40 -0
- package/test/task-routing.test.js +62 -0
- package/test/task-store.test.js +231 -7
- package/test/telegram-reply-topic-routing.test.js +94 -0
- package/test/telegram-task-dispatcher.test.js +150 -23
- package/test/telegram-text-artifact.test.js +13 -2
- package/test/telegram-tools-command.test.js +47 -0
- package/test/telegram-workspace-topic-store.test.js +124 -0
- package/test/tool-registry-run.test.js +41 -0
- package/test/weighted-resource-governor.test.js +95 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
getChatArtifactsDir,
|
|
4
|
+
getChatToolStateDir,
|
|
5
|
+
getChatToolTmpDir,
|
|
6
|
+
getToolStateDir,
|
|
7
|
+
getToolTmpDir
|
|
8
|
+
} from "../../runtime/paths.js";
|
|
9
|
+
import { ToolResourceNoteStore } from "../tools/tool-resource-note-store.js";
|
|
10
|
+
import { installBundledOfficialTool } from "../tools/official-tool-installer.js";
|
|
11
|
+
import { searchOfficialToolCatalog } from "../tools/official-tool-catalog.js";
|
|
12
|
+
import { taskWithoutCallerRouting } from "../tasks/task-routing.js";
|
|
13
|
+
|
|
14
|
+
export const defaultScheduledTaskListLimit = 50;
|
|
15
|
+
export const maxScheduledTaskListLimit = 100;
|
|
16
|
+
|
|
17
|
+
function requireToolName(toolName) {
|
|
18
|
+
if (typeof toolName !== "string" || !toolName.trim()) throw new Error("toolName is required");
|
|
19
|
+
return toolName.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireChatId(chatId, method) {
|
|
23
|
+
if (chatId == null || chatId === "") throw new Error(`${method} requires chatId`);
|
|
24
|
+
return chatId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function requireString(value, fieldName) {
|
|
28
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${fieldName} is required`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeArgs(args) {
|
|
33
|
+
if (args == null) return {};
|
|
34
|
+
if (typeof args !== "object" || Array.isArray(args)) throw new Error("args must be an object");
|
|
35
|
+
return args;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeLimit(limit) {
|
|
39
|
+
const value = Number(limit);
|
|
40
|
+
if (!Number.isInteger(value) || value <= 0) return 20;
|
|
41
|
+
return Math.min(value, 100);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeAcknowledgement(value) {
|
|
45
|
+
if (value == null || value === "") return "";
|
|
46
|
+
const acknowledgement = requireString(value, "acknowledgement").trim();
|
|
47
|
+
if (acknowledgement.length > 500) throw new Error("acknowledgement must be at most 500 characters");
|
|
48
|
+
return acknowledgement;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inferDeliveryMethod(artifact) {
|
|
52
|
+
if (artifact.kind === "audio" || (artifact.mimeType || "").startsWith("audio/")) return "audio";
|
|
53
|
+
if (artifact.kind === "image" || (artifact.mimeType || "").startsWith("image/")) return "photo";
|
|
54
|
+
if (artifact.kind === "video" || (artifact.mimeType || "").startsWith("video/")) return "video";
|
|
55
|
+
return "document";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function containsAbsolutePath(value) {
|
|
59
|
+
if (typeof value !== "string") return false;
|
|
60
|
+
return /(^|\s)(\/[^\s]|[A-Za-z]:[\\/])/.test(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveMediaCaption(caption) {
|
|
64
|
+
return caption && !containsAbsolutePath(caption) ? caption : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function selectScheduledTasks(tasks = [], { status, limit = defaultScheduledTaskListLimit } = {}) {
|
|
68
|
+
const parsedLimit = Number(limit);
|
|
69
|
+
const resolvedLimit = Math.min(
|
|
70
|
+
Math.max(Number.isFinite(parsedLimit) ? Math.trunc(parsedLimit) : defaultScheduledTaskListLimit, 1),
|
|
71
|
+
maxScheduledTaskListLimit
|
|
72
|
+
);
|
|
73
|
+
const allTasks = Array.isArray(tasks) ? tasks : [];
|
|
74
|
+
const orderedTasks = status
|
|
75
|
+
? [...allTasks].reverse()
|
|
76
|
+
: [
|
|
77
|
+
...allTasks.filter((task) => task.status === "pending" || task.status === "running").reverse(),
|
|
78
|
+
...allTasks.filter((task) => task.status !== "pending" && task.status !== "running").reverse()
|
|
79
|
+
];
|
|
80
|
+
const visibleTasks = orderedTasks.slice(0, resolvedLimit);
|
|
81
|
+
return {
|
|
82
|
+
tasks: visibleTasks,
|
|
83
|
+
total: allTasks.length,
|
|
84
|
+
returned: visibleTasks.length,
|
|
85
|
+
limit: resolvedLimit,
|
|
86
|
+
truncated: visibleTasks.length < allTasks.length
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function catalogSearch(searchCatalog, query) {
|
|
91
|
+
try {
|
|
92
|
+
return await searchCatalog(query);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return { unavailable: true, error: error?.message || String(error), matches: [] };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createCapabilityService({
|
|
99
|
+
artifactStore,
|
|
100
|
+
taskStore,
|
|
101
|
+
toolRegistry,
|
|
102
|
+
toolExecutor,
|
|
103
|
+
resourceNotes = new ToolResourceNoteStore(),
|
|
104
|
+
installOfficialTool = installBundledOfficialTool,
|
|
105
|
+
searchCatalog = searchOfficialToolCatalog,
|
|
106
|
+
logger
|
|
107
|
+
} = {}) {
|
|
108
|
+
async function execute({ method, actorToolName, chatId = null, params = {}, context = {} } = {}) {
|
|
109
|
+
const actor = requireToolName(actorToolName);
|
|
110
|
+
if (context.allowedMethods && !context.allowedMethods.has(method)) {
|
|
111
|
+
throw new Error(`unknown ${context.unknownMethodLabel || "capability"} method: ${method}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (method === "tools.list") {
|
|
115
|
+
await toolRegistry.load();
|
|
116
|
+
const query = String(params.query || "").trim();
|
|
117
|
+
const cliTools = query
|
|
118
|
+
? toolRegistry.search(query).map((tool) => ({ ...tool, source: "arisa-modular", invocation: "run_tool" }))
|
|
119
|
+
: (await toolRegistry.listWithRuntime(chatId)).map((tool) => ({ ...tool, source: "arisa-modular", invocation: "run_tool" }));
|
|
120
|
+
const catalogFallback = query && cliTools.length === 0 ? await catalogSearch(searchCatalog, query) : null;
|
|
121
|
+
const coreTools = query ? [] : context.coreTools || [];
|
|
122
|
+
const nativeTools = query ? [] : context.nativeTools || [];
|
|
123
|
+
return {
|
|
124
|
+
query: query || null,
|
|
125
|
+
...(context.workspaceDir ? { workspaceDir: context.workspaceDir } : {}),
|
|
126
|
+
coreTools,
|
|
127
|
+
nativeTools,
|
|
128
|
+
cliTools,
|
|
129
|
+
officialCatalogMatches: Array.isArray(catalogFallback) ? catalogFallback : catalogFallback?.matches || [],
|
|
130
|
+
catalogFallback: catalogFallback && !Array.isArray(catalogFallback) ? catalogFallback : null,
|
|
131
|
+
tools: query ? cliTools : [...coreTools.filter((tool) => tool.enabled !== false), ...nativeTools.filter((tool) => tool.enabled !== false), ...cliTools]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (method === "tools.help") {
|
|
136
|
+
await toolRegistry.load();
|
|
137
|
+
return toolRegistry.help(requireString(params.name, "name"));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (method === "tools.skills") {
|
|
141
|
+
await toolRegistry.load();
|
|
142
|
+
return toolRegistry.resolveSkills(requireString(params.name, "name"));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (method === "tools.setConfig") {
|
|
146
|
+
await toolRegistry.load();
|
|
147
|
+
return toolRegistry.setConfig(
|
|
148
|
+
requireString(params.name, "name"),
|
|
149
|
+
requireString(params.field, "field"),
|
|
150
|
+
requireString(params.value, "value"),
|
|
151
|
+
requireChatId(chatId, method)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (method === "tools.setResourceNote") {
|
|
156
|
+
const name = context.allowTargetToolName && params.name
|
|
157
|
+
? requireString(params.name, "name")
|
|
158
|
+
: actor;
|
|
159
|
+
return resourceNotes.set(
|
|
160
|
+
requireChatId(chatId, method),
|
|
161
|
+
name,
|
|
162
|
+
requireString(params.resourceId, "resourceId"),
|
|
163
|
+
String(params.note ?? "")
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (method === "tools.getResourceNote") {
|
|
168
|
+
const scopedChatId = requireChatId(chatId, method);
|
|
169
|
+
const name = context.allowTargetToolName && params.name
|
|
170
|
+
? requireString(params.name, "name")
|
|
171
|
+
: actor;
|
|
172
|
+
const resourceId = requireString(params.resourceId, "resourceId");
|
|
173
|
+
return { toolName: name, resourceId, note: await resourceNotes.get(scopedChatId, name, resourceId) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (method === "tools.run") {
|
|
177
|
+
if (!toolExecutor?.runTool) throw new Error("tools.run requires toolExecutor");
|
|
178
|
+
const scopedChatId = requireChatId(chatId, method);
|
|
179
|
+
const targetToolName = requireString(params.name, "name");
|
|
180
|
+
const chatArtifactStore = artifactStore.forChat(scopedChatId);
|
|
181
|
+
const artifact = params.artifactId
|
|
182
|
+
? await chatArtifactStore.get(requireString(params.artifactId, "artifactId"))
|
|
183
|
+
: null;
|
|
184
|
+
if (params.artifactId && !artifact) {
|
|
185
|
+
if (context.returnMissingArtifact) return { ok: false, status: "failed", error: `Artifact not found: ${params.artifactId}` };
|
|
186
|
+
throw new Error(`Artifact not found: ${params.artifactId}`);
|
|
187
|
+
}
|
|
188
|
+
const result = await toolExecutor.runTool({
|
|
189
|
+
name: targetToolName,
|
|
190
|
+
request: {
|
|
191
|
+
artifact,
|
|
192
|
+
text: params.text,
|
|
193
|
+
resourceId: params.resourceId,
|
|
194
|
+
args: normalizeArgs(params.args)
|
|
195
|
+
},
|
|
196
|
+
chatId: scopedChatId,
|
|
197
|
+
taskContext: context.taskContext || null
|
|
198
|
+
});
|
|
199
|
+
if (params.deliver && result.output?.artifactId) {
|
|
200
|
+
const generated = await chatArtifactStore.get(result.output.artifactId);
|
|
201
|
+
if (generated?.path) {
|
|
202
|
+
result.sent = await deliverArtifact({
|
|
203
|
+
artifact: generated,
|
|
204
|
+
chatId: scopedChatId,
|
|
205
|
+
method: params.method,
|
|
206
|
+
caption: params.caption,
|
|
207
|
+
context
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (method === "tools.installOfficial") {
|
|
215
|
+
const name = requireString(params.name, "name");
|
|
216
|
+
if (params.confirmName !== name) throw new Error("tools.installOfficial requires confirmName equal to name");
|
|
217
|
+
const installed = await installOfficialTool(name);
|
|
218
|
+
await toolRegistry.load();
|
|
219
|
+
return installed;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (method === "artifacts.createText") {
|
|
223
|
+
const scopedChatId = requireChatId(chatId, method);
|
|
224
|
+
return artifactStore.forChat(scopedChatId).createText({
|
|
225
|
+
text: requireString(params.text, "text"),
|
|
226
|
+
mimeType: params.mimeType || "text/plain",
|
|
227
|
+
source: { type: "tool", toolName: actor, chatId: scopedChatId },
|
|
228
|
+
metadata: params.metadata || {}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (method === "artifacts.listRecent") {
|
|
233
|
+
return artifactStore.forChat(requireChatId(chatId, method)).listRecent(normalizeLimit(params.limit));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (method === "artifacts.get") {
|
|
237
|
+
return artifactStore.forChat(requireChatId(chatId, method)).get(requireString(params.artifactId, "artifactId"));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (method === "artifacts.deliver") {
|
|
241
|
+
const scopedChatId = requireChatId(chatId, method);
|
|
242
|
+
const artifactId = requireString(params.artifactId, "artifactId");
|
|
243
|
+
const artifact = await artifactStore.forChat(scopedChatId).get(artifactId);
|
|
244
|
+
if (!artifact) {
|
|
245
|
+
if (context.returnMissingArtifact) return { ok: false, status: "failed", error: `Artifact not found: ${artifactId}` };
|
|
246
|
+
throw new Error(`Artifact not found or has no file: ${artifactId}`);
|
|
247
|
+
}
|
|
248
|
+
if (!artifact.path) {
|
|
249
|
+
if (context.returnMissingArtifact) return { ok: false, status: "failed", error: `Artifact ${artifactId} has no file to deliver.` };
|
|
250
|
+
throw new Error(`Artifact not found or has no file: ${artifactId}`);
|
|
251
|
+
}
|
|
252
|
+
return deliverArtifact({ artifact, chatId: scopedChatId, caption: params.caption, method: params.method, context });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (method === "tasks.add") {
|
|
256
|
+
const scopedChatId = requireChatId(chatId, method);
|
|
257
|
+
return taskStore.add(taskWithoutCallerRouting(params.task || {}), {
|
|
258
|
+
payload: { chatId: scopedChatId },
|
|
259
|
+
source: { type: "tool", toolName: actor, chatId: scopedChatId }
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (method === "tasks.list") {
|
|
264
|
+
const scopedChatId = requireChatId(chatId, method);
|
|
265
|
+
const tasks = await taskStore.list({ chatId: scopedChatId, status: params.status || undefined, kind: params.kind || undefined });
|
|
266
|
+
return context.selectScheduledTasks
|
|
267
|
+
? selectScheduledTasks(tasks, { status: params.status, limit: params.limit })
|
|
268
|
+
: tasks;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (method === "tasks.cancel") {
|
|
272
|
+
const scopedChatId = requireChatId(chatId, method);
|
|
273
|
+
const taskId = requireString(params.taskId || params.id, "taskId");
|
|
274
|
+
const task = await taskStore.get(taskId);
|
|
275
|
+
if (!task) return context.wrapTaskResult ? { ok: false, error: "Task not found" } : null;
|
|
276
|
+
if (String(task.payload?.chatId) !== String(scopedChatId)) {
|
|
277
|
+
if (context.wrapTaskResult) return { ok: false, error: "Task not found" };
|
|
278
|
+
throw new Error("task does not belong to chatId");
|
|
279
|
+
}
|
|
280
|
+
const cancelled = await taskStore.cancel(taskId);
|
|
281
|
+
return context.wrapTaskResult ? { ok: true, task: cancelled } : cancelled;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (method === "tasks.cancelAll") {
|
|
285
|
+
const tasks = await taskStore.cancelAll({ chatId: requireChatId(chatId, method) });
|
|
286
|
+
return context.wrapTaskResult ? { ok: true, cancelled: tasks.length, tasks } : tasks;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (method === "agent.enqueueEvent") {
|
|
290
|
+
const scopedChatId = requireChatId(chatId, method);
|
|
291
|
+
const resourceId = String(params.resourceId || "").trim();
|
|
292
|
+
return taskStore.add({
|
|
293
|
+
kind: "agent_event",
|
|
294
|
+
payload: {
|
|
295
|
+
prompt: requireString(params.prompt, "prompt"),
|
|
296
|
+
resourceId,
|
|
297
|
+
acknowledgement: normalizeAcknowledgement(params.acknowledgement)
|
|
298
|
+
}
|
|
299
|
+
}, {
|
|
300
|
+
payload: { chatId: scopedChatId },
|
|
301
|
+
source: { type: "tool", toolName: actor, chatId: scopedChatId, resourceId }
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (method === "telegram.createTopic") {
|
|
306
|
+
if (typeof context.telegram?.createForumTopic !== "function") return { ok: false, error: "Telegram topic creation is unavailable in this chat." };
|
|
307
|
+
return context.telegram.createForumTopic(requireString(params.name, "name").trim(), requireString(params.context, "context").trim());
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (method === "telegram.initializeTopic") {
|
|
311
|
+
if (typeof context.telegram?.initializeForumTopic !== "function") return { ok: false, error: "Telegram topic initialization is unavailable in this chat." };
|
|
312
|
+
return context.telegram.initializeForumTopic({
|
|
313
|
+
messageThreadId: params.messageThreadId,
|
|
314
|
+
name: requireString(params.name, "name").trim(),
|
|
315
|
+
context: requireString(params.context, "context").trim()
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (method === "paths.getChatToolStateDir") return getChatToolStateDir(requireChatId(chatId, method), actor);
|
|
320
|
+
if (method === "paths.getToolStateDir") return getToolStateDir(actor);
|
|
321
|
+
if (method === "paths.getChatToolTmpDir") return getChatToolTmpDir(requireChatId(chatId, method), actor);
|
|
322
|
+
if (method === "paths.getToolTmpDir") return getToolTmpDir(actor);
|
|
323
|
+
if (method === "paths.getChatArtifactsDir") return getChatArtifactsDir(requireChatId(chatId, method));
|
|
324
|
+
|
|
325
|
+
throw new Error(`unknown ${context.unknownMethodLabel || "capability"} method: ${method}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function deliverArtifact({ artifact, chatId, caption, method, context }) {
|
|
329
|
+
const resolvedMethod = method || artifact.metadata?.delivery?.method || inferDeliveryMethod(artifact);
|
|
330
|
+
const resolvedCaption = resolveMediaCaption(caption);
|
|
331
|
+
if (typeof context.delivery === "function") {
|
|
332
|
+
logger?.log("capabilities", `deliver artifact ${artifact.id} as ${resolvedMethod}`);
|
|
333
|
+
return context.delivery(artifact, { method: resolvedMethod, caption: resolvedCaption, filename: path.basename(artifact.path) });
|
|
334
|
+
}
|
|
335
|
+
if (!toolExecutor?.deliverArtifact) throw new Error("artifact delivery is unavailable");
|
|
336
|
+
return toolExecutor.deliverArtifact({ chatId, artifact, caption: resolvedCaption, method: resolvedMethod });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return { execute };
|
|
340
|
+
}
|
|
@@ -16,6 +16,12 @@ export const daemonConfigDefaults = Object.freeze({
|
|
|
16
16
|
ipcFrameBytes: 1_048_576
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
export const toolExecutionConfigDefaults = Object.freeze({
|
|
20
|
+
defaultCapacity: 2,
|
|
21
|
+
maxQueuedPerClass: 100,
|
|
22
|
+
capacities: Object.freeze({})
|
|
23
|
+
});
|
|
24
|
+
|
|
19
25
|
export const telegramConfigDefaults = Object.freeze({
|
|
20
26
|
modelPickerPageSize: 8,
|
|
21
27
|
busyMessageMode: "steer",
|
|
@@ -37,7 +43,16 @@ export const cliLogConfig = Object.freeze({
|
|
|
37
43
|
|
|
38
44
|
export const serviceConfigDefaults = Object.freeze({
|
|
39
45
|
shutdownTimeoutMs: 15_000,
|
|
40
|
-
shutdownPollIntervalMs: 100
|
|
46
|
+
shutdownPollIntervalMs: 100,
|
|
47
|
+
workerRestartLimit: 3,
|
|
48
|
+
workerRestartBackoffMs: 2_000,
|
|
49
|
+
workerRestartBackoffMaxMs: 60_000,
|
|
50
|
+
workerStableRuntimeMs: 60_000
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const taskConfigDefaults = Object.freeze({
|
|
54
|
+
agentTimeoutMs: 15 * 60_000,
|
|
55
|
+
eventTimeoutMs: 5 * 60_000
|
|
41
56
|
});
|
|
42
57
|
|
|
43
58
|
export const piConfigDefaults = Object.freeze({
|
|
@@ -70,6 +85,14 @@ export function applyConfigDefaults(config) {
|
|
|
70
85
|
...telegramConfigDefaults,
|
|
71
86
|
...(config.telegram || {})
|
|
72
87
|
},
|
|
88
|
+
toolExecution: {
|
|
89
|
+
...toolExecutionConfigDefaults,
|
|
90
|
+
...(config.toolExecution || {}),
|
|
91
|
+
capacities: {
|
|
92
|
+
...toolExecutionConfigDefaults.capacities,
|
|
93
|
+
...(config.toolExecution?.capacities || {})
|
|
94
|
+
}
|
|
95
|
+
},
|
|
73
96
|
doctor: {
|
|
74
97
|
...doctorConfigDefaults,
|
|
75
98
|
...(config.doctor || {})
|
|
@@ -78,6 +101,10 @@ export function applyConfigDefaults(config) {
|
|
|
78
101
|
...serviceConfigDefaults,
|
|
79
102
|
...(config.service || {})
|
|
80
103
|
},
|
|
104
|
+
tasks: {
|
|
105
|
+
...taskConfigDefaults,
|
|
106
|
+
...(config.tasks || {})
|
|
107
|
+
},
|
|
81
108
|
pi: {
|
|
82
109
|
...piConfigDefaults,
|
|
83
110
|
...configuredPi,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
function errorMessage(error) {
|
|
2
|
+
return error instanceof Error ? error.message : String(error);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class NonRetryableTaskError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "NonRetryableTaskError";
|
|
9
|
+
this.retryable = false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createTaskRunner({ taskStore, dispatch, laneKey = (task) => task.id, onTerminalFailure, logger, claimLimit = 10 }) {
|
|
14
|
+
if (!taskStore || typeof dispatch !== "function") {
|
|
15
|
+
throw new Error("Task runner requires taskStore and dispatch");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function reportTerminalFailure(task, updated, error) {
|
|
19
|
+
const isTerminal = updated?.status === "failed"
|
|
20
|
+
|| updated?.status === "outcome_uncertain"
|
|
21
|
+
|| updated?.terminalFailure === true;
|
|
22
|
+
if (!isTerminal || typeof onTerminalFailure !== "function") return;
|
|
23
|
+
try {
|
|
24
|
+
await onTerminalFailure({ task, result: updated, error });
|
|
25
|
+
} catch (notificationError) {
|
|
26
|
+
logger?.log("tasks", `task ${task.id} failure notification failed: ${errorMessage(notificationError)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lanes = new Map();
|
|
31
|
+
|
|
32
|
+
async function executeClaimedTask(task) {
|
|
33
|
+
try {
|
|
34
|
+
await taskStore.markExecutionStarted?.(task.id);
|
|
35
|
+
await dispatch(task);
|
|
36
|
+
await taskStore.complete(task.id);
|
|
37
|
+
logger?.log("tasks", `task ${task.id} completed after confirmed execution`);
|
|
38
|
+
return { taskId: task.id, status: "completed" };
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const retryOptions = { retryable: error?.retryable !== false };
|
|
41
|
+
if (error?.outcomeUncertain === true) retryOptions.outcomeUncertain = true;
|
|
42
|
+
const updated = await taskStore.retryOrFail(task.id, error, retryOptions);
|
|
43
|
+
const status = updated?.status || "missing";
|
|
44
|
+
logger?.log("tasks", `task ${task.id} ${status}: ${errorMessage(error)}`);
|
|
45
|
+
await reportTerminalFailure(task, updated, error);
|
|
46
|
+
return { taskId: task.id, status, error: errorMessage(error) };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runClaimedTask(task) {
|
|
51
|
+
const key = String(laneKey(task));
|
|
52
|
+
const previous = lanes.get(key) || Promise.resolve();
|
|
53
|
+
const running = previous.catch(() => {}).then(() => executeClaimedTask(task));
|
|
54
|
+
lanes.set(key, running);
|
|
55
|
+
running.then(
|
|
56
|
+
() => { if (lanes.get(key) === running) lanes.delete(key); },
|
|
57
|
+
() => { if (lanes.get(key) === running) lanes.delete(key); }
|
|
58
|
+
);
|
|
59
|
+
return running;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function dispatchDueTasks() {
|
|
63
|
+
const tasks = await taskStore.claimDue(claimLimit);
|
|
64
|
+
return Promise.all(tasks.map(runClaimedTask));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { dispatchDueTasks, runClaimedTask };
|
|
68
|
+
}
|