@ykurilov/agent-chat-background 0.0.0-stage → 0.55.0
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/CHANGELOG.md +369 -0
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/chunk-4RA3JT6K.js +9 -0
- package/dist/chunk-4RA3JT6K.js.map +1 -0
- package/dist/chunk-4XFSUFM6.js +53 -0
- package/dist/chunk-4XFSUFM6.js.map +1 -0
- package/dist/index.js +756 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.js +14 -0
- package/dist/labels.js.map +1 -0
- package/dist/postgres.js +212 -0
- package/dist/postgres.js.map +1 -0
- package/dist/sqlite.js +184 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/types/background-tasks.d.ts +18 -0
- package/dist/types/background-tool.d.ts +16 -0
- package/dist/types/helper-task.d.ts +17 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/keyed-lock.d.ts +3 -0
- package/dist/types/labels.d.ts +10 -0
- package/dist/types/postgres.d.ts +9 -0
- package/dist/types/sqlite.d.ts +8 -0
- package/dist/types/store/memory.d.ts +3 -0
- package/dist/types/store/migration-settings.d.ts +4 -0
- package/dist/types/store/postgres-schema.d.ts +406 -0
- package/dist/types/store/postgres.d.ts +12 -0
- package/dist/types/store/sqlite-schema.d.ts +431 -0
- package/dist/types/store/sqlite.d.ts +6 -0
- package/dist/types/store/task-store.d.ts +23 -0
- package/dist/types/task-delivery.d.ts +24 -0
- package/dist/types/task-runner.d.ts +51 -0
- package/dist/types/task-tools.d.ts +13 -0
- package/dist/types/types.d.ts +97 -0
- package/drizzle/postgres/0000_init.sql +28 -0
- package/drizzle/postgres/0001_delivery_key.sql +1 -0
- package/drizzle/postgres/meta/0000_snapshot.json +229 -0
- package/drizzle/postgres/meta/0001_snapshot.json +235 -0
- package/drizzle/postgres/meta/_journal.json +20 -0
- package/drizzle/sqlite/0000_init.sql +28 -0
- package/drizzle/sqlite/0001_delivery_key.sql +1 -0
- package/drizzle/sqlite/meta/0000_snapshot.json +208 -0
- package/drizzle/sqlite/meta/0001_snapshot.json +215 -0
- package/drizzle/sqlite/meta/_journal.json +20 -0
- package/package.json +75 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NOTICE_STATUSES,
|
|
3
|
+
RUNNING_STATUSES,
|
|
4
|
+
isFinishedStatus,
|
|
5
|
+
isRunningStatus,
|
|
6
|
+
meetsCondition,
|
|
7
|
+
patchEntries,
|
|
8
|
+
withoutEmptyFields
|
|
9
|
+
} from "./chunk-4XFSUFM6.js";
|
|
10
|
+
|
|
11
|
+
// src/background-tool.ts
|
|
12
|
+
function backgroundTool(definition) {
|
|
13
|
+
return definition;
|
|
14
|
+
}
|
|
15
|
+
function turnFields(model) {
|
|
16
|
+
return { ownerId: model.ownerId, chatId: model.chatId, messageId: model.messageId ?? "", channel: model.channel ?? "web" };
|
|
17
|
+
}
|
|
18
|
+
var TOO_MANY_TASKS = (max) => `\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u043D\u043E\u0433\u043E \u0444\u043E\u043D\u043E\u0432\u044B\u0445 \u0437\u0430\u0434\u0430\u0447: \u043E\u0434\u043D\u043E\u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u0438\u0434\u0451\u0442 \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${max}. \u0414\u043E\u0436\u0434\u0438\u0442\u0435\u0441\u044C, \u043F\u043E\u043A\u0430 \u043E\u0434\u043D\u0430 \u0438\u0437 \u043D\u0438\u0445 \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u0442\u0441\u044F, \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u0435 \u043D\u0435\u043D\u0443\u0436\u043D\u0443\u044E (cancel_task).`;
|
|
19
|
+
function backgroundToolForTurn(name, definition, model, service) {
|
|
20
|
+
return {
|
|
21
|
+
description: definition.description,
|
|
22
|
+
inputSchema: definition.inputSchema,
|
|
23
|
+
...definition.needsApproval !== void 0 && { needsApproval: definition.needsApproval },
|
|
24
|
+
...definition.metadata !== void 0 && { metadata: definition.metadata },
|
|
25
|
+
async execute(input, options) {
|
|
26
|
+
const title = definition.title(input);
|
|
27
|
+
const started = await service.runner().start({ ...turnFields(model), toolCallId: options.toolCallId, kind: "tool", name, input, title }, service.maxRunningPerUser);
|
|
28
|
+
if (started.status === "too_many_tasks") return { status: "too_many_tasks", message: TOO_MANY_TASKS(service.maxRunningPerUser) };
|
|
29
|
+
return { status: "started", taskId: started.taskId, title };
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function runBackgroundTool(definition, record, control) {
|
|
34
|
+
const result = await definition.run(record.input, {
|
|
35
|
+
ownerId: record.ownerId,
|
|
36
|
+
chatId: record.chatId,
|
|
37
|
+
signal: control.signal,
|
|
38
|
+
reportProgress: control.reportProgress
|
|
39
|
+
});
|
|
40
|
+
return { status: "done", result };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/background-tasks.ts
|
|
44
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
45
|
+
|
|
46
|
+
// src/helper-task.ts
|
|
47
|
+
import { jsonSchema } from "ai";
|
|
48
|
+
var HELPER_TOOL_PREFIX = "helper_";
|
|
49
|
+
var TITLE_MAX_LENGTH = 80;
|
|
50
|
+
var helperInput = jsonSchema({
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: { task: { type: "string", description: "\u0417\u0430\u0434\u0430\u043D\u0438\u0435 \u043F\u043E\u043C\u043E\u0449\u043D\u0438\u043A\u0443: \u0447\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u0442\u044C \u0438 \u0447\u0442\u043E \u0432\u0435\u0440\u043D\u0443\u0442\u044C" } },
|
|
53
|
+
required: ["task"],
|
|
54
|
+
additionalProperties: false
|
|
55
|
+
});
|
|
56
|
+
function backgroundHelpers(port) {
|
|
57
|
+
const helpers = port?.helpers ?? {};
|
|
58
|
+
return Object.fromEntries(Object.entries(helpers).filter(([, helper]) => helper.background === true));
|
|
59
|
+
}
|
|
60
|
+
function helperModelOutput(output) {
|
|
61
|
+
const progress = output;
|
|
62
|
+
if (progress?.status === "background") {
|
|
63
|
+
return { type: "text", value: `\u041F\u043E\u043C\u043E\u0449\u043D\u0438\u043A \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432 \u0444\u043E\u043D\u0435${progress.taskId ? ` (\u0437\u0430\u0434\u0430\u0447\u0430 ${progress.taskId})` : ""}: \u0438\u0442\u043E\u0433 \u043F\u0440\u0438\u0434\u0451\u0442 \u043F\u043E\u0437\u0436\u0435.` };
|
|
64
|
+
}
|
|
65
|
+
return { type: "text", value: `\u041F\u043E\u043C\u043E\u0449\u043D\u0438\u043A \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0441\u044F: ${progress?.error ?? "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430"}` };
|
|
66
|
+
}
|
|
67
|
+
function helperToolForTurn(name, helper, model, service) {
|
|
68
|
+
return {
|
|
69
|
+
description: helper.description,
|
|
70
|
+
inputSchema: helperInput,
|
|
71
|
+
toModelOutput: ({ output }) => helperModelOutput(output),
|
|
72
|
+
async execute({ task }, options) {
|
|
73
|
+
const started = await service.runner().start(
|
|
74
|
+
{ ...turnFields(model), toolCallId: options.toolCallId, kind: "helper", name, input: { task }, title: titleOf(task) },
|
|
75
|
+
service.maxRunningPerUser
|
|
76
|
+
);
|
|
77
|
+
if (started.status === "too_many_tasks") {
|
|
78
|
+
const message = TOO_MANY_TASKS(service.maxRunningPerUser);
|
|
79
|
+
return { task, steps: [], text: message, status: "failed", error: message };
|
|
80
|
+
}
|
|
81
|
+
return { task, steps: [], text: "", status: "background", taskId: started.taskId };
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function runHelperTask(port, helper, record, control) {
|
|
86
|
+
const { task } = record.input;
|
|
87
|
+
let lastProgress = record.progress;
|
|
88
|
+
const model = await port.helperModel(record.name, record.ownerId, record.chatId);
|
|
89
|
+
const result = await port.runHelper({
|
|
90
|
+
name: record.name,
|
|
91
|
+
helper,
|
|
92
|
+
task,
|
|
93
|
+
model,
|
|
94
|
+
...record.helperMessages && { priorMessages: record.helperMessages },
|
|
95
|
+
abortSignal: control.signal,
|
|
96
|
+
async onStep(step) {
|
|
97
|
+
lastProgress = step.progress;
|
|
98
|
+
await control.saveStep({ helperMessages: step.messages, progress: step.progress });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const progress = { ...lastProgress ?? { task, steps: [] }, text: result.text, status: result.status };
|
|
102
|
+
switch (result.status) {
|
|
103
|
+
case "done":
|
|
104
|
+
return { status: "done", result: result.text, helperMessages: result.messages, progress };
|
|
105
|
+
case "failed":
|
|
106
|
+
return { status: "failed", error: result.text, helperMessages: result.messages, progress: { ...progress, error: result.text } };
|
|
107
|
+
case "awaiting_approval":
|
|
108
|
+
return { status: "awaiting_approval", helperMessages: result.messages, progress: lastProgress ?? progress };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function titleOf(task) {
|
|
112
|
+
const line = task.trim().replace(/\s+/g, " ");
|
|
113
|
+
return line.length <= TITLE_MAX_LENGTH ? line : `${line.slice(0, TITLE_MAX_LENGTH - 1)}\u2026`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/task-delivery.ts
|
|
117
|
+
var NOTICE_RESULT_MAX_LENGTH = 5e4;
|
|
118
|
+
var isQuiet = (task) => task.status === "canceled" || task.status === "awaiting_approval";
|
|
119
|
+
function createTaskDelivery(deps) {
|
|
120
|
+
const { store, leaseOwner } = deps;
|
|
121
|
+
const pending = /* @__PURE__ */ new Map();
|
|
122
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
123
|
+
const again = /* @__PURE__ */ new Set();
|
|
124
|
+
const chatKey = (ownerId, chatId) => `${ownerId}\0${chatId}`;
|
|
125
|
+
async function deliverOnce(ownerId, chatId) {
|
|
126
|
+
const tasks = (await store.listUndelivered(ownerId, chatId)).filter((task) => task.leaseOwner === leaseOwner);
|
|
127
|
+
const delivered = [];
|
|
128
|
+
for (const group of await batchesOf(ownerId, tasks)) {
|
|
129
|
+
const result = await deps.port().deliverNotices({
|
|
130
|
+
ownerId,
|
|
131
|
+
chatId,
|
|
132
|
+
notices: group.tasks.map(noticeOf),
|
|
133
|
+
wakeAgent: !group.tasks.every(isQuiet),
|
|
134
|
+
deliveryKey: group.key
|
|
135
|
+
});
|
|
136
|
+
if (result.status === "busy") return "wait";
|
|
137
|
+
if (result.status === "chat_gone") {
|
|
138
|
+
const left = tasks.filter((task) => !delivered.includes(task.id));
|
|
139
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
140
|
+
for (const task of left) {
|
|
141
|
+
await store.update(
|
|
142
|
+
ownerId,
|
|
143
|
+
task.id,
|
|
144
|
+
{ status: "canceled", finishedAt: task.finishedAt ?? now, deliveredAt: now, leaseOwner: void 0, leaseUntil: void 0 },
|
|
145
|
+
{ leaseOwner, statuses: NOTICE_STATUSES }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return "done";
|
|
149
|
+
}
|
|
150
|
+
const ids = group.tasks.map((task) => task.id);
|
|
151
|
+
await store.markDelivered(ids, group.key, (/* @__PURE__ */ new Date()).toISOString());
|
|
152
|
+
delivered.push(...ids);
|
|
153
|
+
}
|
|
154
|
+
return "done";
|
|
155
|
+
}
|
|
156
|
+
async function batchesOf(ownerId, tasks) {
|
|
157
|
+
const batches = /* @__PURE__ */ new Map();
|
|
158
|
+
for (const task of tasks) if (task.deliveryKey) batches.set(task.deliveryKey, [...batches.get(task.deliveryKey) ?? [], task]);
|
|
159
|
+
const fresh = tasks.filter((task) => !task.deliveryKey);
|
|
160
|
+
for (const group of [fresh.filter(isQuiet), fresh.filter((task) => !isQuiet(task))]) {
|
|
161
|
+
if (group.length === 0) continue;
|
|
162
|
+
const key = group.map(deliveryKeyOf).join("+");
|
|
163
|
+
const kept = [];
|
|
164
|
+
for (const task of group) if (await store.update(ownerId, task.id, { deliveryKey: key }, { leaseOwner })) kept.push(task);
|
|
165
|
+
if (kept.length > 0) batches.set(key, kept);
|
|
166
|
+
}
|
|
167
|
+
const quietFirst = (group) => group.every(isQuiet) ? 0 : 1;
|
|
168
|
+
return [...batches].map(([key, group]) => ({ key, tasks: group })).sort((a, b) => quietFirst(a.tasks) - quietFirst(b.tasks));
|
|
169
|
+
}
|
|
170
|
+
function drain(ownerId, chatId) {
|
|
171
|
+
const key = chatKey(ownerId, chatId);
|
|
172
|
+
const running = inFlight.get(key);
|
|
173
|
+
if (running) {
|
|
174
|
+
again.add(key);
|
|
175
|
+
return running;
|
|
176
|
+
}
|
|
177
|
+
const work = (async () => {
|
|
178
|
+
try {
|
|
179
|
+
let outcome;
|
|
180
|
+
do {
|
|
181
|
+
again.delete(key);
|
|
182
|
+
outcome = await deliverOnce(ownerId, chatId).catch((error) => {
|
|
183
|
+
deps.port().logError(error);
|
|
184
|
+
return "wait";
|
|
185
|
+
});
|
|
186
|
+
} while (again.has(key));
|
|
187
|
+
if (outcome === "done") pending.delete(key);
|
|
188
|
+
} finally {
|
|
189
|
+
inFlight.delete(key);
|
|
190
|
+
}
|
|
191
|
+
})();
|
|
192
|
+
inFlight.set(key, work);
|
|
193
|
+
return work;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
request(ownerId, chatId) {
|
|
197
|
+
pending.set(chatKey(ownerId, chatId), { ownerId, chatId });
|
|
198
|
+
return drain(ownerId, chatId);
|
|
199
|
+
},
|
|
200
|
+
chatFree(ownerId, chatId) {
|
|
201
|
+
if (pending.has(chatKey(ownerId, chatId))) void drain(ownerId, chatId);
|
|
202
|
+
},
|
|
203
|
+
async retryPending() {
|
|
204
|
+
await Promise.all([...pending.values()].map(({ ownerId, chatId }) => drain(ownerId, chatId)));
|
|
205
|
+
},
|
|
206
|
+
forget(ownerId, chatId) {
|
|
207
|
+
pending.delete(chatKey(ownerId, chatId));
|
|
208
|
+
},
|
|
209
|
+
async idle() {
|
|
210
|
+
await Promise.allSettled([...inFlight.values()]);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function deliveryKeyOf(task) {
|
|
215
|
+
const approvalId = task.status === "awaiting_approval" ? task.progress?.pendingApproval?.approvalId : void 0;
|
|
216
|
+
return approvalId ? `task:${task.id}:approval:${approvalId}` : `task:${task.id}`;
|
|
217
|
+
}
|
|
218
|
+
function noticeOf(task) {
|
|
219
|
+
const base = { taskId: task.id, title: task.title };
|
|
220
|
+
switch (task.status) {
|
|
221
|
+
case "done":
|
|
222
|
+
return {
|
|
223
|
+
kind: "task_done",
|
|
224
|
+
...base,
|
|
225
|
+
...task.result !== void 0 && { result: noticeResult(task.result) },
|
|
226
|
+
// Помощник брал чужой текст: чат с этим итогом живые демонстрации не запускает.
|
|
227
|
+
...task.progress?.externalContent && { externalContent: true }
|
|
228
|
+
};
|
|
229
|
+
case "failed":
|
|
230
|
+
return { kind: "task_failed", ...base, error: task.error ?? "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430" };
|
|
231
|
+
case "timed_out":
|
|
232
|
+
return { kind: "task_timed_out", ...base, ...task.error && { error: task.error } };
|
|
233
|
+
case "canceled":
|
|
234
|
+
return { kind: "task_canceled", ...base };
|
|
235
|
+
case "awaiting_approval": {
|
|
236
|
+
const pendingApproval = task.progress?.pendingApproval;
|
|
237
|
+
return {
|
|
238
|
+
kind: "task_awaiting_approval",
|
|
239
|
+
...base,
|
|
240
|
+
...pendingApproval && { approval: { approvalId: pendingApproval.approvalId, toolName: pendingApproval.toolName, input: pendingApproval.input } }
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
return { kind: "task_interrupted", ...base };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function noticeResult(result) {
|
|
248
|
+
const text = typeof result === "string" ? result : JSON.stringify(result);
|
|
249
|
+
if (text.length <= NOTICE_RESULT_MAX_LENGTH) return text;
|
|
250
|
+
const tail = `
|
|
251
|
+
\u2026 (\u0438\u0442\u043E\u0433 \u043E\u0431\u0440\u0435\u0437\u0430\u043D: \u0432\u0441\u0435\u0433\u043E ${text.length} \u0437\u043D\u0430\u043A\u043E\u0432)`;
|
|
252
|
+
return text.slice(0, NOTICE_RESULT_MAX_LENGTH - tail.length) + tail;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/task-runner.ts
|
|
256
|
+
import { randomUUID } from "crypto";
|
|
257
|
+
|
|
258
|
+
// src/keyed-lock.ts
|
|
259
|
+
function createKeyedLock() {
|
|
260
|
+
const tails = /* @__PURE__ */ new Map();
|
|
261
|
+
return (key, work) => {
|
|
262
|
+
const result = (tails.get(key) ?? Promise.resolve()).then(work);
|
|
263
|
+
const tail = result.then(
|
|
264
|
+
() => {
|
|
265
|
+
},
|
|
266
|
+
() => {
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
tails.set(key, tail);
|
|
270
|
+
void tail.then(() => {
|
|
271
|
+
if (tails.get(key) === tail) tails.delete(key);
|
|
272
|
+
});
|
|
273
|
+
return result;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/task-runner.ts
|
|
278
|
+
var MAX_ATTEMPTS = 3;
|
|
279
|
+
var CANCELABLE_STATUSES = [...RUNNING_STATUSES, "awaiting_approval"];
|
|
280
|
+
function createTaskRunner(deps) {
|
|
281
|
+
const { store, leaseOwner: me, leaseMs, delivery } = deps;
|
|
282
|
+
const leases = /* @__PURE__ */ new Map();
|
|
283
|
+
const taskLock = createKeyedLock();
|
|
284
|
+
const ownerLock = createKeyedLock();
|
|
285
|
+
let stopped = false;
|
|
286
|
+
let recovering;
|
|
287
|
+
const now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
288
|
+
const leaseUntil = (from) => new Date(Date.parse(from) + leaseMs).toISOString();
|
|
289
|
+
const stillMine = { leaseOwner: me, statuses: RUNNING_STATUSES };
|
|
290
|
+
const tasksChanged = (ownerId, chatId) => deps.port().events.emit({ type: "tasks-changed", ownerId, chatId });
|
|
291
|
+
const logError = (error) => deps.port().logError(error);
|
|
292
|
+
function stopRun(run, reason) {
|
|
293
|
+
run.stopped = true;
|
|
294
|
+
run.finishing = true;
|
|
295
|
+
clearTimeout(run.timer);
|
|
296
|
+
run.controller.abort(new Error(reason));
|
|
297
|
+
}
|
|
298
|
+
async function writeOutcome(record, outcome) {
|
|
299
|
+
const at = now();
|
|
300
|
+
const patch = { ...outcome, ...isFinishedStatus(outcome.status) && { finishedAt: at }, leaseUntil: leaseUntil(at) };
|
|
301
|
+
if (!await store.update(record.ownerId, record.id, patch, stillMine)) {
|
|
302
|
+
leases.delete(record.id);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
leases.set(record.id, { ownerId: record.ownerId, chatId: record.chatId });
|
|
306
|
+
tasksChanged(record.ownerId, record.chatId);
|
|
307
|
+
void delivery.request(record.ownerId, record.chatId);
|
|
308
|
+
}
|
|
309
|
+
async function finish(run, outcome) {
|
|
310
|
+
if (run.finishing) return;
|
|
311
|
+
run.finishing = true;
|
|
312
|
+
clearTimeout(run.timer);
|
|
313
|
+
if (outcome.status === "timed_out") run.controller.abort(new Error(outcome.error));
|
|
314
|
+
await taskLock(run.record.id, async () => {
|
|
315
|
+
if (run.stopped) return;
|
|
316
|
+
try {
|
|
317
|
+
await writeOutcome(run.record, outcome);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
leases.delete(run.record.id);
|
|
320
|
+
logError(error);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function progressWriter(run) {
|
|
325
|
+
let latest;
|
|
326
|
+
let writing = false;
|
|
327
|
+
return (text) => {
|
|
328
|
+
latest = text;
|
|
329
|
+
if (writing) return;
|
|
330
|
+
writing = true;
|
|
331
|
+
void (async () => {
|
|
332
|
+
try {
|
|
333
|
+
while (latest !== void 0 && !run.finishing) {
|
|
334
|
+
const next = latest;
|
|
335
|
+
latest = void 0;
|
|
336
|
+
const progress = { task: run.record.title, steps: [], text: next, status: "running" };
|
|
337
|
+
if (await store.update(run.record.ownerId, run.record.id, { progress }, stillMine)) tasksChanged(run.record.ownerId, run.record.chatId);
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
logError(error);
|
|
341
|
+
} finally {
|
|
342
|
+
writing = false;
|
|
343
|
+
}
|
|
344
|
+
})();
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function launch(record, plan) {
|
|
348
|
+
const run = { record, controller: new AbortController(), finishing: false, stopped: false };
|
|
349
|
+
leases.set(record.id, { ownerId: record.ownerId, chatId: record.chatId, run });
|
|
350
|
+
run.timer = setTimeout(() => void finish(run, { status: "timed_out", error: `\u0417\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0430\u0441\u044C \u0432 ${formatDuration(plan.timeoutMs)}` }), plan.timeoutMs);
|
|
351
|
+
run.timer.unref?.();
|
|
352
|
+
const control = {
|
|
353
|
+
signal: run.controller.signal,
|
|
354
|
+
reportProgress: progressWriter(run),
|
|
355
|
+
async saveStep(patch) {
|
|
356
|
+
if (run.finishing) return;
|
|
357
|
+
if (!await store.update(record.ownerId, record.id, { ...patch, leaseUntil: leaseUntil(now()) }, stillMine)) {
|
|
358
|
+
leases.delete(record.id);
|
|
359
|
+
stopRun(run, "\u0430\u0440\u0435\u043D\u0434\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430");
|
|
360
|
+
throw run.controller.signal.reason;
|
|
361
|
+
}
|
|
362
|
+
tasksChanged(record.ownerId, record.chatId);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
void Promise.resolve().then(() => plan.work(record, control)).then(
|
|
366
|
+
(outcome) => finish(run, outcome),
|
|
367
|
+
(error) => finish(run, { status: "failed", error: messageOf(error) })
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
async function finishWithout(record, outcome) {
|
|
371
|
+
await taskLock(record.id, () => writeOutcome(record, outcome));
|
|
372
|
+
}
|
|
373
|
+
async function recoverOnce() {
|
|
374
|
+
for (const stale of await store.listExpired(now())) {
|
|
375
|
+
if (stopped) return;
|
|
376
|
+
if (leases.has(stale.id)) continue;
|
|
377
|
+
try {
|
|
378
|
+
const at = now();
|
|
379
|
+
const claimed = await store.claim(stale.id, me, leaseUntil(at), at);
|
|
380
|
+
if (claimed) await resume(stale.status, claimed);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
logError(error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function resume(prior, claimed) {
|
|
387
|
+
if (!isRunningStatus(prior)) {
|
|
388
|
+
leases.set(claimed.id, { ownerId: claimed.ownerId, chatId: claimed.chatId });
|
|
389
|
+
await delivery.request(claimed.ownerId, claimed.chatId);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const plan = deps.plan(claimed);
|
|
393
|
+
if (typeof plan === "string") return finishWithout(claimed, { status: "failed", error: plan });
|
|
394
|
+
if (claimed.attempt > MAX_ATTEMPTS) return finishWithout(claimed, { status: "interrupted" });
|
|
395
|
+
if (prior === "running" && !plan.rerunAfterRestart) return finishWithout(claimed, { status: "interrupted" });
|
|
396
|
+
launch(claimed, plan);
|
|
397
|
+
tasksChanged(claimed.ownerId, claimed.chatId);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
start(task, maxRunningPerUser) {
|
|
401
|
+
return ownerLock(task.ownerId, async () => {
|
|
402
|
+
if (await store.countActive(task.ownerId) >= maxRunningPerUser) return { status: "too_many_tasks" };
|
|
403
|
+
const plan = deps.plan({ ...task, id: "", status: "queued", attempt: 0, createdAt: "" });
|
|
404
|
+
if (typeof plan === "string") throw new Error(plan);
|
|
405
|
+
const createdAt = now();
|
|
406
|
+
const id = randomUUID();
|
|
407
|
+
await store.create({ ...task, id, status: "queued", attempt: 0, createdAt, leaseOwner: me, leaseUntil: leaseUntil(createdAt) });
|
|
408
|
+
const claimed = await store.claim(id, me, leaseUntil(createdAt), createdAt);
|
|
409
|
+
if (!claimed) throw new Error(`\u0417\u0430\u0434\u0430\u0447\u0430 ${id} \u043D\u0435 \u0432\u0437\u044F\u0442\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0443 \u0441\u0440\u0430\u0437\u0443 \u043F\u043E\u0441\u043B\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F`);
|
|
410
|
+
launch(claimed, plan);
|
|
411
|
+
tasksChanged(task.ownerId, task.chatId);
|
|
412
|
+
return { status: "started", taskId: id };
|
|
413
|
+
});
|
|
414
|
+
},
|
|
415
|
+
cancel(ownerId, chatId, taskId, by) {
|
|
416
|
+
return taskLock(taskId, async () => {
|
|
417
|
+
const task = await store.get(ownerId, taskId);
|
|
418
|
+
if (!task || task.chatId !== chatId) return "not_found";
|
|
419
|
+
if (isFinishedStatus(task.status)) return "already_finished";
|
|
420
|
+
const at = now();
|
|
421
|
+
const canceled = by === "human" ? (
|
|
422
|
+
// Пометка «отменена» в чате: доставит эта копия, агент не проснётся.
|
|
423
|
+
{ status: "canceled", finishedAt: at, leaseOwner: me, leaseUntil: leaseUntil(at), deliveredAt: void 0, deliveryKey: void 0 }
|
|
424
|
+
) : (
|
|
425
|
+
// Отменил сам агент: он и так знает, сообщать нечего.
|
|
426
|
+
{ status: "canceled", finishedAt: at, deliveredAt: at, leaseOwner: void 0, leaseUntil: void 0, deliveryKey: void 0 }
|
|
427
|
+
);
|
|
428
|
+
if (!await store.update(ownerId, taskId, canceled, { statuses: CANCELABLE_STATUSES })) return "already_finished";
|
|
429
|
+
const run = leases.get(taskId)?.run;
|
|
430
|
+
if (run) stopRun(run, "\u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430");
|
|
431
|
+
if (by === "human") {
|
|
432
|
+
leases.set(taskId, { ownerId, chatId });
|
|
433
|
+
void delivery.request(ownerId, chatId);
|
|
434
|
+
} else {
|
|
435
|
+
leases.delete(taskId);
|
|
436
|
+
}
|
|
437
|
+
tasksChanged(ownerId, chatId);
|
|
438
|
+
return "canceled";
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
answerApproval(ownerId, chatId, taskId, approvalId, decision) {
|
|
442
|
+
return taskLock(taskId, async () => {
|
|
443
|
+
const task = await store.get(ownerId, taskId);
|
|
444
|
+
const pending = task?.progress?.pendingApproval;
|
|
445
|
+
if (!task || task.chatId !== chatId || task.status !== "awaiting_approval" || !pending) return "not_found";
|
|
446
|
+
if (pending.approvalId !== approvalId) return "stale";
|
|
447
|
+
const plan = deps.plan(task);
|
|
448
|
+
if (typeof plan === "string") return "not_found";
|
|
449
|
+
const { pendingApproval: _answered, ...progress } = task.progress;
|
|
450
|
+
const response = { type: "tool-approval-response", approvalId, approved: decision === "allow" };
|
|
451
|
+
const resumed = await store.update(
|
|
452
|
+
ownerId,
|
|
453
|
+
taskId,
|
|
454
|
+
{
|
|
455
|
+
status: "running",
|
|
456
|
+
helperMessages: [...task.helperMessages ?? [], { role: "tool", content: [response] }],
|
|
457
|
+
progress: { ...progress, status: "running" },
|
|
458
|
+
deliveredAt: void 0,
|
|
459
|
+
deliveryKey: void 0,
|
|
460
|
+
leaseOwner: me,
|
|
461
|
+
leaseUntil: leaseUntil(now())
|
|
462
|
+
},
|
|
463
|
+
{ statuses: ["awaiting_approval"], pendingApprovalId: approvalId }
|
|
464
|
+
);
|
|
465
|
+
if (!resumed) return (await store.get(ownerId, taskId))?.status === "awaiting_approval" ? "stale" : "not_found";
|
|
466
|
+
launch(resumed, plan);
|
|
467
|
+
tasksChanged(ownerId, chatId);
|
|
468
|
+
return "resumed";
|
|
469
|
+
});
|
|
470
|
+
},
|
|
471
|
+
async chatDeleted(ownerId, chatId) {
|
|
472
|
+
for (const [taskId, lease] of leases) {
|
|
473
|
+
if (lease.ownerId !== ownerId || lease.chatId !== chatId) continue;
|
|
474
|
+
if (lease.run) stopRun(lease.run, "\u0447\u0430\u0442 \u0443\u0434\u0430\u043B\u0451\u043D");
|
|
475
|
+
leases.delete(taskId);
|
|
476
|
+
}
|
|
477
|
+
delivery.forget(ownerId, chatId);
|
|
478
|
+
await store.deleteForChat(ownerId, chatId);
|
|
479
|
+
},
|
|
480
|
+
recoverExpired() {
|
|
481
|
+
recovering ??= recoverOnce().finally(() => {
|
|
482
|
+
recovering = void 0;
|
|
483
|
+
});
|
|
484
|
+
return recovering;
|
|
485
|
+
},
|
|
486
|
+
async renewLeases() {
|
|
487
|
+
const until = leaseUntil(now());
|
|
488
|
+
await Promise.all(
|
|
489
|
+
[...leases].map(async ([taskId, lease]) => {
|
|
490
|
+
let renewed;
|
|
491
|
+
try {
|
|
492
|
+
renewed = await store.renewLease(taskId, me, until);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
logError(error);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (renewed || leases.get(taskId) !== lease) return;
|
|
498
|
+
leases.delete(taskId);
|
|
499
|
+
if (lease.run) stopRun(lease.run, "\u0430\u0440\u0435\u043D\u0434\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430");
|
|
500
|
+
})
|
|
501
|
+
);
|
|
502
|
+
},
|
|
503
|
+
async stop() {
|
|
504
|
+
stopped = true;
|
|
505
|
+
const held = [...leases.entries()];
|
|
506
|
+
leases.clear();
|
|
507
|
+
for (const [, lease] of held) if (lease.run) stopRun(lease.run, "\u0441\u043B\u0443\u0436\u0431\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430");
|
|
508
|
+
const at = now();
|
|
509
|
+
await Promise.all(held.map(([taskId]) => store.renewLease(taskId, me, at).catch(logError)));
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function messageOf(error) {
|
|
514
|
+
return error instanceof Error ? error.message : String(error);
|
|
515
|
+
}
|
|
516
|
+
function formatDuration(ms) {
|
|
517
|
+
return ms >= 6e4 && ms % 6e4 === 0 ? `${ms / 6e4} \u043C\u0438\u043D` : `${Math.ceil(ms / 1e3)} \u0441`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/task-tools.ts
|
|
521
|
+
import { jsonSchema as jsonSchema2 } from "ai";
|
|
522
|
+
var CHECK_TASKS = "check_tasks";
|
|
523
|
+
var CANCEL_TASK = "cancel_task";
|
|
524
|
+
function backgroundInstructions(maxRunningPerUser) {
|
|
525
|
+
return [
|
|
526
|
+
"\u0424\u043E\u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438. \u041D\u0435\u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442 \u0432 \u0444\u043E\u043D\u0435: \u0432\u044B\u0437\u043E\u0432 \u0441\u0440\u0430\u0437\u0443 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \xABstarted\xBB \u0441 taskId, \u0438\u0442\u043E\u0433 \u043F\u0440\u0438\u0434\u0451\u0442 \u043F\u043E\u0437\u0436\u0435 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u043C \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u043C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435\u043C.",
|
|
527
|
+
"\u041D\u0435 \u0436\u0434\u0438 \u0438\u0442\u043E\u0433\u0430 \u0438 \u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439 \u0442\u0443 \u0436\u0435 \u0437\u0430\u0434\u0430\u0447\u0443 \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E: \u0441\u043A\u0430\u0436\u0438 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044E, \u0447\u0442\u043E \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430, \u0438 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440.",
|
|
528
|
+
`\u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447 \u044D\u0442\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u2014 ${CHECK_TASKS}. \u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 \u2014 ${CANCEL_TASK} \u0441 \u0435\u0451 taskId.`,
|
|
529
|
+
`\u0423 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u043E\u0434\u043D\u043E\u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u0438\u0434\u0451\u0442 \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${maxRunningPerUser} \u0437\u0430\u0434\u0430\u0447; \u043F\u0440\u0438 \u043E\u0442\u0432\u0435\u0442\u0435 \xABtoo_many_tasks\xBB \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u043E\u0434\u043D\u0443 \u0438\u0437 \u043D\u0438\u0445.`
|
|
530
|
+
].join("\n");
|
|
531
|
+
}
|
|
532
|
+
function taskToolsForTurn(model, service) {
|
|
533
|
+
const { ownerId, chatId } = model;
|
|
534
|
+
return {
|
|
535
|
+
[CHECK_TASKS]: {
|
|
536
|
+
description: "\u0424\u043E\u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u044D\u0442\u043E\u0433\u043E \u0447\u0430\u0442\u0430: \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435, \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435, \u0445\u043E\u0434.",
|
|
537
|
+
inputSchema: jsonSchema2({ type: "object", properties: {}, additionalProperties: false }),
|
|
538
|
+
async execute() {
|
|
539
|
+
const tasks = await service.store.listForChat(ownerId, chatId);
|
|
540
|
+
return {
|
|
541
|
+
tasks: tasks.map((task) => ({
|
|
542
|
+
taskId: task.id,
|
|
543
|
+
title: task.title,
|
|
544
|
+
status: task.status,
|
|
545
|
+
startedAt: task.startedAt ?? task.createdAt,
|
|
546
|
+
...task.finishedAt && { finishedAt: task.finishedAt },
|
|
547
|
+
...task.progress?.text && { progress: task.progress.text },
|
|
548
|
+
...task.error && { error: task.error }
|
|
549
|
+
}))
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
[CANCEL_TASK]: {
|
|
554
|
+
description: "\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0444\u043E\u043D\u043E\u0432\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443 \u044D\u0442\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u043F\u043E taskId.",
|
|
555
|
+
inputSchema: jsonSchema2({
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: { taskId: { type: "string" } },
|
|
558
|
+
required: ["taskId"],
|
|
559
|
+
additionalProperties: false
|
|
560
|
+
}),
|
|
561
|
+
async execute({ taskId }) {
|
|
562
|
+
return { status: await service.runner().cancel(ownerId, chatId, taskId, "agent"), taskId };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/background-tasks.ts
|
|
569
|
+
var DEFAULT_MAX_RUNNING_PER_USER = 3;
|
|
570
|
+
var DEFAULT_TIMEOUT_MS = 30 * 6e4;
|
|
571
|
+
var DEFAULT_LEASE_MS = 3e4;
|
|
572
|
+
var MIN_LEASE_MS = 3e3;
|
|
573
|
+
var TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
574
|
+
function createBackgroundTasks(options) {
|
|
575
|
+
const { store } = options;
|
|
576
|
+
const tools = options.tools ?? {};
|
|
577
|
+
const maxRunningPerUser = options.maxRunningPerUser ?? DEFAULT_MAX_RUNNING_PER_USER;
|
|
578
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
579
|
+
const leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
580
|
+
checkOptions(tools, maxRunningPerUser, timeoutMs, leaseMs);
|
|
581
|
+
const leaseOwner = randomUUID2();
|
|
582
|
+
let port;
|
|
583
|
+
const requirePort = () => {
|
|
584
|
+
if (!port) throw new Error("\u0424\u043E\u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u044B \u043A \u0441\u043B\u0443\u0436\u0431\u0435 \u0447\u0430\u0442\u0430: \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0438\u0445 \u0432 createChatServer \u2014 \u044F\u0434\u0440\u043E \u0432\u044B\u0437\u043E\u0432\u0435\u0442 attach");
|
|
585
|
+
return port;
|
|
586
|
+
};
|
|
587
|
+
function plan(record) {
|
|
588
|
+
if (record.kind === "tool") {
|
|
589
|
+
const definition = tools[record.name];
|
|
590
|
+
if (!definition) return `\u0424\u043E\u043D\u043E\u0432\u043E\u0433\u043E \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0430 \xAB${record.name}\xBB \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0442 \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u0441\u043B\u0443\u0436\u0431\u044B`;
|
|
591
|
+
return {
|
|
592
|
+
work: (task, control) => runBackgroundTool(definition, task, control),
|
|
593
|
+
rerunAfterRestart: definition.rerunAfterRestart === true,
|
|
594
|
+
timeoutMs: definition.timeoutMs ?? timeoutMs
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const helper = backgroundHelpers(port)[record.name];
|
|
598
|
+
if (!helper) return `\u0424\u043E\u043D\u043E\u0432\u043E\u0433\u043E \u043F\u043E\u043C\u043E\u0449\u043D\u0438\u043A\u0430 \xAB${record.name}\xBB \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0442 \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u0441\u043B\u0443\u0436\u0431\u044B`;
|
|
599
|
+
return { work: (task, control) => runHelperTask(requirePort(), helper, task, control), rerunAfterRestart: true, timeoutMs };
|
|
600
|
+
}
|
|
601
|
+
const delivery = createTaskDelivery({ store, leaseOwner, port: requirePort });
|
|
602
|
+
const runner = createTaskRunner({ store, leaseOwner, leaseMs, port: requirePort, delivery, plan });
|
|
603
|
+
const service = { store, maxRunningPerUser, runner: () => (requirePort(), runner) };
|
|
604
|
+
let timer;
|
|
605
|
+
let unsubscribe;
|
|
606
|
+
let recovering = false;
|
|
607
|
+
let renewing;
|
|
608
|
+
let sweeping;
|
|
609
|
+
function tick() {
|
|
610
|
+
renewing ??= runner.renewLeases().finally(() => {
|
|
611
|
+
renewing = void 0;
|
|
612
|
+
});
|
|
613
|
+
sweeping ??= (async () => {
|
|
614
|
+
await delivery.retryPending();
|
|
615
|
+
if (recovering) await runner.recoverExpired().catch((error) => port?.logError(error));
|
|
616
|
+
})().finally(() => {
|
|
617
|
+
sweeping = void 0;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
instructions: backgroundInstructions(maxRunningPerUser),
|
|
622
|
+
get toolNames() {
|
|
623
|
+
return [...Object.keys(tools), ...Object.keys(backgroundHelpers(port)).map((name) => HELPER_TOOL_PREFIX + name), CHECK_TASKS, CANCEL_TASK];
|
|
624
|
+
},
|
|
625
|
+
tools(model) {
|
|
626
|
+
const set = {};
|
|
627
|
+
for (const [name, definition] of Object.entries(tools)) set[name] = backgroundToolForTurn(name, definition, model, service);
|
|
628
|
+
for (const [name, helper] of Object.entries(backgroundHelpers(port))) set[HELPER_TOOL_PREFIX + name] = helperToolForTurn(name, helper, model, service);
|
|
629
|
+
return { ...set, ...taskToolsForTurn(model, service) };
|
|
630
|
+
},
|
|
631
|
+
attach(attached) {
|
|
632
|
+
if (port) throw new Error("\u0424\u043E\u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0436\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u044B \u043A \u0441\u043B\u0443\u0436\u0431\u0435 \u0447\u0430\u0442\u0430: \u043E\u0434\u0438\u043D \u044D\u043A\u0437\u0435\u043C\u043F\u043B\u044F\u0440 \u2014 \u043E\u0434\u043D\u0430 \u0441\u043B\u0443\u0436\u0431\u0430");
|
|
633
|
+
port = attached;
|
|
634
|
+
unsubscribe = attached.events.subscribe((event) => {
|
|
635
|
+
if (event.type === "turn-finished") delivery.chatFree(event.ownerId, event.chatId);
|
|
636
|
+
});
|
|
637
|
+
timer = setInterval(tick, Math.floor(leaseMs / 3));
|
|
638
|
+
timer.unref?.();
|
|
639
|
+
},
|
|
640
|
+
async listForChat(ownerId, chatId) {
|
|
641
|
+
return (await store.listForChat(ownerId, chatId)).map(viewOf);
|
|
642
|
+
},
|
|
643
|
+
cancel: (ownerId, chatId, taskId) => service.runner().cancel(ownerId, chatId, taskId, "human"),
|
|
644
|
+
answerApproval: (ownerId, chatId, taskId, approvalId, decision) => service.runner().answerApproval(ownerId, chatId, taskId, approvalId, decision),
|
|
645
|
+
chatDeleted: (ownerId, chatId) => service.runner().chatDeleted(ownerId, chatId),
|
|
646
|
+
async recover() {
|
|
647
|
+
requirePort();
|
|
648
|
+
recovering = true;
|
|
649
|
+
await runner.recoverExpired();
|
|
650
|
+
},
|
|
651
|
+
async flush() {
|
|
652
|
+
clearInterval(timer);
|
|
653
|
+
unsubscribe?.();
|
|
654
|
+
recovering = false;
|
|
655
|
+
await Promise.all([renewing, sweeping]);
|
|
656
|
+
await runner.stop();
|
|
657
|
+
await delivery.idle();
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function checkOptions(tools, maxRunningPerUser, timeoutMs, leaseMs) {
|
|
662
|
+
if (!Number.isInteger(maxRunningPerUser) || maxRunningPerUser < 1) throw new Error("\u0424\u043E\u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438: maxRunningPerUser \u2014 \u0446\u0435\u043B\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u043E\u0442 1");
|
|
663
|
+
if (!(timeoutMs > 0)) throw new Error("\u0424\u043E\u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438: timeoutMs \u2014 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0443\u043B\u044F");
|
|
664
|
+
if (!(leaseMs >= MIN_LEASE_MS)) throw new Error(`\u0424\u043E\u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438: leaseMs \u2014 \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 ${MIN_LEASE_MS} \u043C\u0441`);
|
|
665
|
+
for (const [name, definition] of Object.entries(tools)) {
|
|
666
|
+
if (!TOOL_NAME.test(name)) throw new Error(`\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442 \xAB${name}\xBB: \u0438\u043C\u044F \u2014 \u043B\u0430\u0442\u0438\u043D\u0438\u0446\u0430, \u0446\u0438\u0444\u0440\u044B, \xAB_\xBB \u0438 \xAB-\xBB, \u0434\u043E 64 \u0437\u043D\u0430\u043A\u043E\u0432`);
|
|
667
|
+
if (name === CHECK_TASKS || name === CANCEL_TASK || name.startsWith(HELPER_TOOL_PREFIX)) {
|
|
668
|
+
throw new Error(`\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442 \xAB${name}\xBB: \u0438\u043C\u044F \u0437\u0430\u043D\u044F\u0442\u043E \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0430\u043C\u0438 \u0444\u043E\u043D\u0430 \u0438\u043B\u0438 \u043F\u043E\u043C\u043E\u0449\u043D\u0438\u043A\u043E\u0432`);
|
|
669
|
+
}
|
|
670
|
+
if (definition.timeoutMs !== void 0 && !(definition.timeoutMs > 0)) throw new Error(`\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442 \xAB${name}\xBB: timeoutMs \u2014 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0443\u043B\u044F`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function viewOf(task) {
|
|
674
|
+
return {
|
|
675
|
+
id: task.id,
|
|
676
|
+
title: task.title,
|
|
677
|
+
kind: task.kind,
|
|
678
|
+
name: task.name,
|
|
679
|
+
status: task.status,
|
|
680
|
+
startedAt: task.startedAt ?? task.createdAt,
|
|
681
|
+
...task.finishedAt && { finishedAt: task.finishedAt },
|
|
682
|
+
...task.progress && { progress: task.progress }
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/store/memory.ts
|
|
687
|
+
var copy = (record) => withoutEmptyFields(structuredClone(record));
|
|
688
|
+
var byCreatedAt = (left, right) => left.createdAt.localeCompare(right.createdAt);
|
|
689
|
+
function memoryTaskStore() {
|
|
690
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
691
|
+
const owned = (ownerId, taskId) => {
|
|
692
|
+
const task = tasks.get(taskId);
|
|
693
|
+
return task && task.ownerId === ownerId ? task : void 0;
|
|
694
|
+
};
|
|
695
|
+
const isFree = (task, leaseOwner, now) => task.leaseOwner === leaseOwner || task.leaseUntil === void 0 || task.leaseUntil < now;
|
|
696
|
+
return {
|
|
697
|
+
async create(task) {
|
|
698
|
+
tasks.set(task.id, copy(task));
|
|
699
|
+
},
|
|
700
|
+
async get(ownerId, taskId) {
|
|
701
|
+
const task = owned(ownerId, taskId);
|
|
702
|
+
return task ? copy(task) : null;
|
|
703
|
+
},
|
|
704
|
+
async update(ownerId, taskId, patch, when) {
|
|
705
|
+
const task = owned(ownerId, taskId);
|
|
706
|
+
if (!task || !meetsCondition(task, when)) return null;
|
|
707
|
+
const changed = { ...task };
|
|
708
|
+
for (const [key, value] of patchEntries(structuredClone(patch))) changed[key] = value;
|
|
709
|
+
const next = withoutEmptyFields(changed);
|
|
710
|
+
tasks.set(taskId, next);
|
|
711
|
+
return copy(next);
|
|
712
|
+
},
|
|
713
|
+
async listForChat(ownerId, chatId) {
|
|
714
|
+
return [...tasks.values()].filter((task) => task.ownerId === ownerId && task.chatId === chatId).sort(byCreatedAt).map(copy);
|
|
715
|
+
},
|
|
716
|
+
async countActive(ownerId) {
|
|
717
|
+
return [...tasks.values()].filter((task) => task.ownerId === ownerId && isRunningStatus(task.status)).length;
|
|
718
|
+
},
|
|
719
|
+
async claim(taskId, leaseOwner, leaseUntil, now) {
|
|
720
|
+
const task = tasks.get(taskId);
|
|
721
|
+
if (!task || task.deliveredAt !== void 0 || !isFree(task, leaseOwner, now)) return null;
|
|
722
|
+
const next = isRunningStatus(task.status) ? { ...task, status: "running", attempt: task.attempt + 1, startedAt: task.startedAt ?? now, leaseOwner, leaseUntil } : { ...task, leaseOwner, leaseUntil };
|
|
723
|
+
tasks.set(taskId, next);
|
|
724
|
+
return copy(next);
|
|
725
|
+
},
|
|
726
|
+
async renewLease(taskId, leaseOwner, leaseUntil) {
|
|
727
|
+
const task = tasks.get(taskId);
|
|
728
|
+
if (!task || task.deliveredAt !== void 0 || task.leaseOwner !== leaseOwner) return false;
|
|
729
|
+
tasks.set(taskId, { ...task, leaseUntil });
|
|
730
|
+
return true;
|
|
731
|
+
},
|
|
732
|
+
async listExpired(now) {
|
|
733
|
+
return [...tasks.values()].filter((task) => task.deliveredAt === void 0 && (task.leaseUntil === void 0 || task.leaseUntil < now)).map(copy);
|
|
734
|
+
},
|
|
735
|
+
async listUndelivered(ownerId, chatId) {
|
|
736
|
+
return [...tasks.values()].filter((task) => task.ownerId === ownerId && task.chatId === chatId && task.deliveredAt === void 0).filter((task) => NOTICE_STATUSES.includes(task.status)).sort(byCreatedAt).map(copy);
|
|
737
|
+
},
|
|
738
|
+
async markDelivered(taskIds, deliveryKey, at) {
|
|
739
|
+
for (const taskId of taskIds) {
|
|
740
|
+
const task = tasks.get(taskId);
|
|
741
|
+
if (!task || task.deliveryKey !== deliveryKey || !NOTICE_STATUSES.includes(task.status)) continue;
|
|
742
|
+
const { leaseOwner: _owner, leaseUntil: _until, ...rest } = task;
|
|
743
|
+
tasks.set(taskId, { ...rest, deliveredAt: at });
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
async deleteForChat(ownerId, chatId) {
|
|
747
|
+
for (const [taskId, task] of tasks) if (task.ownerId === ownerId && task.chatId === chatId) tasks.delete(taskId);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
export {
|
|
752
|
+
backgroundTool,
|
|
753
|
+
createBackgroundTasks,
|
|
754
|
+
memoryTaskStore
|
|
755
|
+
};
|
|
756
|
+
//# sourceMappingURL=index.js.map
|