@ykurilov/agent-chat-flows 0.76.0 → 0.78.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 +25 -0
- package/README.md +4 -0
- package/dist/{chunk-PLY57QT5.js → chunk-CW6VSYZC.js} +2 -2
- package/dist/{chunk-DQQ2GCQ2.js → chunk-CZ3EDVYW.js} +17 -7
- package/dist/chunk-CZ3EDVYW.js.map +1 -0
- package/dist/{chunk-2LAVG3YR.js → chunk-RMWVLMGY.js} +1 -1
- package/dist/chunk-RMWVLMGY.js.map +1 -0
- package/dist/format.js +2 -2
- package/dist/index.js +253 -15
- package/dist/index.js.map +1 -1
- package/dist/react.js +2 -2
- package/dist/types/engine/confirmation.d.ts +35 -0
- package/dist/types/engine/flow-run.d.ts +17 -0
- package/dist/types/engine/index.d.ts +1 -0
- package/dist/types/runtime/confirmation-judge.d.ts +26 -0
- package/dist/types/runtime/confirmation-migration.d.ts +12 -0
- package/dist/types/runtime/confirmations.d.ts +40 -0
- package/dist/types/runtime/flow-events.d.ts +1 -0
- package/dist/types/runtime/flow-types.d.ts +18 -0
- package/dist/types/runtime/index.d.ts +2 -0
- package/package.json +4 -4
- package/dist/chunk-2LAVG3YR.js.map +0 -1
- package/dist/chunk-DQQ2GCQ2.js.map +0 -1
- /package/dist/{chunk-PLY57QT5.js.map → chunk-CW6VSYZC.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
sideTaskFieldKnown,
|
|
8
8
|
sideTaskRecord,
|
|
9
9
|
sideTaskStanding
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-CW6VSYZC.js";
|
|
11
11
|
import {
|
|
12
12
|
FACT_PREFIX,
|
|
13
13
|
FIRST_MESSAGE_RETRY_MS,
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
isFactEntry,
|
|
45
45
|
judgeExits,
|
|
46
46
|
pageTimeZone,
|
|
47
|
+
pendingEvent,
|
|
47
48
|
personTimeZone,
|
|
48
49
|
reportTransition,
|
|
49
50
|
runOutsideTurn,
|
|
@@ -56,7 +57,7 @@ import {
|
|
|
56
57
|
transitionReport,
|
|
57
58
|
withPending,
|
|
58
59
|
withReopen
|
|
59
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-CZ3EDVYW.js";
|
|
60
61
|
import {
|
|
61
62
|
AGENT_FLOW_FORMAT,
|
|
62
63
|
AGENT_FLOW_FORMAT_VERSION,
|
|
@@ -140,7 +141,7 @@ import {
|
|
|
140
141
|
withDayAnchor,
|
|
141
142
|
withNudgeStep,
|
|
142
143
|
withoutUndeliveredTouch
|
|
143
|
-
} from "./chunk-
|
|
144
|
+
} from "./chunk-RMWVLMGY.js";
|
|
144
145
|
import {
|
|
145
146
|
appliedInTime,
|
|
146
147
|
byVersionAndState,
|
|
@@ -167,10 +168,175 @@ var FLOW_SERVICE_BLOCKS = [
|
|
|
167
168
|
}
|
|
168
169
|
];
|
|
169
170
|
|
|
171
|
+
// src/runtime/confirmations.ts
|
|
172
|
+
import { createHash } from "crypto";
|
|
173
|
+
var confirmationHash = (spec) => createHash("sha256").update(spec.judge).digest("hex");
|
|
174
|
+
function confirmationHolds(record, spec, scope) {
|
|
175
|
+
return record?.status === "confirmed" && record.key === spec.key && record.revision === spec.revision && record.criterionHash === confirmationHash(spec) && record.scope?.ownerId === scope.ownerId && record.scope.chatId === scope.chatId && record.scope.flowId === scope.flowId && scope.flowId === spec.flowId && record.source?.kind === "judge" && typeof record.source.versionId === "string" && record.source.versionId.length > 0 && typeof record.source.answerId === "string" && record.source.answerId.length > 0 && typeof record.source.humanMessageId === "string" && record.source.humanMessageId.length > 0 && Number.isFinite(record.source.probability) && record.source.probability >= 0 && record.source.probability <= 1;
|
|
176
|
+
}
|
|
177
|
+
function createConfirmations(specs = [], guards = []) {
|
|
178
|
+
const registered = structuredClone(specs);
|
|
179
|
+
const registeredGuards = structuredClone(guards);
|
|
180
|
+
const short = (value) => typeof value === "string" && value.trim().length > 0 && value.length <= 100;
|
|
181
|
+
const seen = /* @__PURE__ */ new Set();
|
|
182
|
+
for (const spec of registered) {
|
|
183
|
+
const id = `${spec.flowId}\0${spec.key}`;
|
|
184
|
+
if (![spec.flowId, spec.key, spec.revision].every(short) || !spec.judge?.trim() || spec.judge.length > 800 || !Array.isArray(spec.states) || !spec.states.length || !spec.states.every(short) || seen.has(id)) throw new TypeError("confirmations: \u043D\u0443\u0436\u043D\u044B \u0443\u043D\u0438\u043A\u0430\u043B\u044C\u043D\u044B\u0435 flowId/key, revision, states \u0438 judge \u0434\u043E 800 \u0437\u043D\u0430\u043A\u043E\u0432");
|
|
185
|
+
seen.add(id);
|
|
186
|
+
}
|
|
187
|
+
const seenGuards = /* @__PURE__ */ new Set();
|
|
188
|
+
for (const guard of registeredGuards) {
|
|
189
|
+
const id = `${guard.flowId}\0${guard.state}\0${guard.exitId}`;
|
|
190
|
+
if (![guard.flowId, guard.state, guard.exitId].every(short) || guard.judgeWindow !== "current-human-turn" || seenGuards.has(id) || !registered.some((spec) => spec.flowId === guard.flowId && spec.key === guard.requires.key && spec.revision === guard.requires.revision)) throw new TypeError("exitGuards: \u043E\u0434\u0438\u043D guard \u043D\u0430 \u0432\u044B\u0445\u043E\u0434, \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 requires \u0438 current-human-turn");
|
|
191
|
+
seenGuards.add(id);
|
|
192
|
+
}
|
|
193
|
+
const specFor = (flowId, ref) => registered.find((spec) => spec.flowId === flowId && spec.key === ref.key && spec.revision === ref.revision);
|
|
194
|
+
const guardFor = (flowId, state, exitId) => registeredGuards.find((guard) => guard.flowId === flowId && guard.state === state && guard.exitId === exitId);
|
|
195
|
+
return {
|
|
196
|
+
validate(flowId, flow) {
|
|
197
|
+
for (const guard of registeredGuards.filter((item) => item.flowId === flowId)) {
|
|
198
|
+
const state = flow.states.find((item) => item.id === guard.state);
|
|
199
|
+
const exit = state && activeExits(flow, state).find((item) => item.exit.id === guard.exitId)?.exit;
|
|
200
|
+
if (exit && exit.when.type !== "judge") throw new TypeError(`exitGuards: ${flowId}/${guard.state}/${guard.exitId} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C judge-\u0432\u044B\u0445\u043E\u0434\u043E\u043C`);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
forState: (flowId, state) => registered.filter((spec) => spec.flowId === flowId && spec.states.includes(state)),
|
|
204
|
+
specFor,
|
|
205
|
+
guardFor,
|
|
206
|
+
revision: (flowId) => createHash("sha256").update(JSON.stringify([registered.filter((spec) => spec.flowId === flowId), registeredGuards.filter((guard) => guard.flowId === flowId)])).digest("hex"),
|
|
207
|
+
allows(scope, run, exitId) {
|
|
208
|
+
const guard = guardFor(scope.flowId, run.state, exitId);
|
|
209
|
+
if (!guard) return true;
|
|
210
|
+
const spec = specFor(scope.flowId, guard.requires);
|
|
211
|
+
const decision = run.checkedTransition;
|
|
212
|
+
return confirmationHolds(run.confirmations?.[spec.key], spec, scope) && decision?.transition.exitId === exitId && decision.answerId === run.answerId && decision.requires.key === spec.key && decision.requires.revision === spec.revision && decision.criterionHash === confirmationHash(spec);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/runtime/confirmation-judge.ts
|
|
218
|
+
import { flowStateCommitter } from "@ykurilov/agent-chat-core";
|
|
219
|
+
var textOf = (message) => message.parts.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
|
|
220
|
+
var messagesOf = (messages) => messages.flatMap((message) => {
|
|
221
|
+
const text = textOf(message).trim();
|
|
222
|
+
return text && (message.role === "user" || message.role === "assistant") ? [{ role: message.role === "user" ? "human" : "agent", text }] : [];
|
|
223
|
+
});
|
|
224
|
+
async function judgeConfirmations(input) {
|
|
225
|
+
const { confirmations, pinned, run, result, port } = input;
|
|
226
|
+
const configurationHash = confirmations.revision(pinned.flowId);
|
|
227
|
+
if (run.confirmationCheck?.answerId === result.messageId && run.confirmationCheck.configurationHash === configurationHash) return { saved: true };
|
|
228
|
+
const scope = { ownerId: result.ownerId, chatId: result.chatId, flowId: pinned.flowId };
|
|
229
|
+
const commit2 = flowStateCommitter(port);
|
|
230
|
+
if (!commit2) throw new Error("confirmations: \u0441\u043B\u0443\u0436\u0431\u0430 \u0447\u0430\u0442\u0430 \u043D\u0435 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u044E\u044E flows capability");
|
|
231
|
+
const all = (await port.peekHistory(scope.ownerId, scope.chatId)).messages;
|
|
232
|
+
const answerIndex = all.findIndex((message) => message.id === result.messageId);
|
|
233
|
+
if (answerIndex < 0) return { saved: false };
|
|
234
|
+
const branch = all.slice(0, answerIndex + 1);
|
|
235
|
+
const humanIndex = branch.findLastIndex((message) => message.role === "user" && Boolean(textOf(message).trim()));
|
|
236
|
+
if (humanIndex < 0) return { saved: false };
|
|
237
|
+
const humanMessageId = branch[humanIndex].id;
|
|
238
|
+
const records = { ...run.confirmations };
|
|
239
|
+
const onBranch = (record) => record?.status === "confirmed" && branch.some((message) => message.id === record.source.answerId) && branch.some((message) => message.id === record.source.humanMessageId);
|
|
240
|
+
const query = async (conditions, messages) => input.judge.checkAll({
|
|
241
|
+
...scope,
|
|
242
|
+
conditions,
|
|
243
|
+
messages,
|
|
244
|
+
...input.goal && { goal: input.goal },
|
|
245
|
+
...run.timeOffset !== void 0 && { testChat: true }
|
|
246
|
+
});
|
|
247
|
+
for (const spec of confirmations.forState(scope.flowId, run.state)) {
|
|
248
|
+
const old = records[spec.key];
|
|
249
|
+
if (confirmationHolds(old, spec, scope) && onBranch(old)) continue;
|
|
250
|
+
const recordedVersion = old?.status === "confirmed" ? old.source?.versionId : old?.versionId;
|
|
251
|
+
if (recordedVersion === pinned.versionId && (old.revision !== spec.revision || old.criterionHash !== confirmationHash(spec))) continue;
|
|
252
|
+
let evidence = input.messages;
|
|
253
|
+
if (old?.status === "revoked" && old.scope.ownerId === scope.ownerId && old.scope.chatId === scope.chatId && old.scope.flowId === scope.flowId) {
|
|
254
|
+
const boundary = old.afterMessageId === null ? -1 : branch.findIndex((message) => message.id === old.afterMessageId);
|
|
255
|
+
if (old.afterMessageId !== null && boundary < 0 || humanIndex <= boundary) continue;
|
|
256
|
+
evidence = messagesOf(branch.slice(boundary + 1)).slice(-6);
|
|
257
|
+
}
|
|
258
|
+
const id = `confirmation:${spec.key}`;
|
|
259
|
+
const verdict = (await query([{ exitId: id, text: spec.judge }], evidence)).find((item) => item.exitId === id && item.confirmed && item.probability >= input.threshold);
|
|
260
|
+
if (!verdict) continue;
|
|
261
|
+
records[spec.key] = {
|
|
262
|
+
key: spec.key,
|
|
263
|
+
revision: spec.revision,
|
|
264
|
+
status: "confirmed",
|
|
265
|
+
scope,
|
|
266
|
+
criterionHash: confirmationHash(spec),
|
|
267
|
+
checkedAt: input.now,
|
|
268
|
+
source: { kind: "judge", versionId: pinned.versionId, answerId: result.messageId, humanMessageId, probability: verdict.probability }
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const next = { ...run, confirmationCheck: { answerId: result.messageId, configurationHash }, ...Object.keys(records).length && { confirmations: records } };
|
|
272
|
+
const ordinary = input.conditions.filter((condition) => !confirmations.guardFor(scope.flowId, run.state, condition.exitId));
|
|
273
|
+
const guarded2 = input.conditions.filter((condition) => {
|
|
274
|
+
const guard = confirmations.guardFor(scope.flowId, run.state, condition.exitId);
|
|
275
|
+
if (!guard) return false;
|
|
276
|
+
const spec = confirmations.specFor(scope.flowId, guard.requires);
|
|
277
|
+
const receipt = records[spec.key];
|
|
278
|
+
return confirmationHolds(receipt, spec, scope) && onBranch(receipt);
|
|
279
|
+
});
|
|
280
|
+
const verdicts = [
|
|
281
|
+
...ordinary.length ? await query(ordinary, input.messages) : [],
|
|
282
|
+
...guarded2.length ? await query(guarded2, messagesOf([branch[humanIndex], branch[answerIndex]])) : []
|
|
283
|
+
];
|
|
284
|
+
const met = input.conditions.map(({ exitId }) => verdicts.find((verdict) => verdict.exitId === exitId && verdict.confirmed && verdict.probability >= input.threshold)).find(Boolean);
|
|
285
|
+
const transition = met ? nextTransition(pinned.flow, next, { type: "verdict", exitId: met.exitId, probability: met.probability, decidedBy: "judge" }, input.now) : null;
|
|
286
|
+
if (transition) {
|
|
287
|
+
const guard = confirmations.guardFor(scope.flowId, run.state, transition.exitId);
|
|
288
|
+
if (guard) next.checkedTransition = { versionId: pinned.versionId, transition, requires: guard.requires, criterionHash: confirmationHash(confirmations.specFor(scope.flowId, guard.requires)), answerId: result.messageId, humanMessageId };
|
|
289
|
+
}
|
|
290
|
+
const changed = JSON.stringify(next) !== JSON.stringify(run);
|
|
291
|
+
if (!changed) return { ...transition && { transition }, saved: true };
|
|
292
|
+
for (const wait2 of [0, 10, 50, 100, 250, 500, 1e3]) {
|
|
293
|
+
if (wait2) await new Promise((resolve) => setTimeout(resolve, wait2));
|
|
294
|
+
const saved = await commit2({ ...scope, revision: confirmations.revision(scope.flowId), expectedAtMessageId: input.atMessageId, idempotencyKey: `judge:${result.messageId}`, flow: next });
|
|
295
|
+
if (saved.status === "applied" || saved.status === "same") return { ...transition && { transition }, saved: true };
|
|
296
|
+
if (saved.status !== "busy") return { saved: false };
|
|
297
|
+
}
|
|
298
|
+
return { saved: false };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/runtime/confirmation-migration.ts
|
|
302
|
+
async function previewConfirmedMigration(input, deps) {
|
|
303
|
+
const { store, confirmations } = deps;
|
|
304
|
+
const spec = confirmations.specFor(input.flowId, input.requiresConfirmation);
|
|
305
|
+
if (!spec) throw new TypeError("previewMigration: requiresConfirmation \u043D\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C");
|
|
306
|
+
const eligible = [];
|
|
307
|
+
const keepOriginal = [];
|
|
308
|
+
const rows = await store.listFlowChats({ flowId: input.flowId, exceptVersionId: input.toVersionId, ...input.chatIds && { chatIds: input.chatIds } });
|
|
309
|
+
for (const row of rows) {
|
|
310
|
+
if (row.endedAt && !input.includeEnded) continue;
|
|
311
|
+
const port = await deps.portFor(row);
|
|
312
|
+
const data = await port.getChatData(row.ownerId, row.chatId);
|
|
313
|
+
const test = testChatOffset(data) !== null;
|
|
314
|
+
if (test && !input.includeTestChats) continue;
|
|
315
|
+
if (input.filter && !await input.filter({ chatId: row.chatId, ownerId: row.ownerId, flowId: row.flowId, versionId: row.versionId, stateId: row.stateId, ended: row.endedAt !== null, endReason: row.endReason, test, data })) continue;
|
|
316
|
+
const snapshot = await port.getChatState(row.ownerId, row.chatId);
|
|
317
|
+
const run = storedRun(snapshot.values.flow);
|
|
318
|
+
const receipt = run?.confirmations?.[spec.key];
|
|
319
|
+
const scope = { ownerId: row.ownerId, chatId: row.chatId, flowId: row.flowId };
|
|
320
|
+
let reason;
|
|
321
|
+
if (!receipt) reason = "missing_receipt";
|
|
322
|
+
else if (receipt.status === "revoked") reason = "revoked_receipt";
|
|
323
|
+
else if (!confirmationHolds(receipt, spec, scope)) reason = "incompatible_receipt";
|
|
324
|
+
else if (!await store.getVersion(row.flowId, receipt.source.versionId)) reason = "source_version_missing";
|
|
325
|
+
else {
|
|
326
|
+
const branch = (await port.peekHistory(row.ownerId, row.chatId)).messages;
|
|
327
|
+
if (!branch.some((message) => message.id === receipt.source.answerId) || !branch.some((message) => message.id === receipt.source.humanMessageId)) reason = "source_not_on_branch";
|
|
328
|
+
}
|
|
329
|
+
if (reason) keepOriginal.push({ chatId: row.chatId, status: "ambiguous", reason });
|
|
330
|
+
else eligible.push(row.chatId);
|
|
331
|
+
}
|
|
332
|
+
return { eligible, keepOriginal };
|
|
333
|
+
}
|
|
334
|
+
|
|
170
335
|
// src/runtime/agent-flow.ts
|
|
171
|
-
import { createHash, randomUUID as randomUUID4 } from "crypto";
|
|
336
|
+
import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
|
|
172
337
|
import {
|
|
173
|
-
ChatNotFoundError as ChatNotFoundError4
|
|
338
|
+
ChatNotFoundError as ChatNotFoundError4,
|
|
339
|
+
flowStateCommitter as flowStateCommitter2
|
|
174
340
|
} from "@ykurilov/agent-chat-core";
|
|
175
341
|
|
|
176
342
|
// src/runtime/turn-tool-names.ts
|
|
@@ -1606,6 +1772,7 @@ function flowExitTool(input, model) {
|
|
|
1606
1772
|
const { flow } = turn.pinned;
|
|
1607
1773
|
const chosen = judgeExits(flow, turn.run.state)[exit - 1];
|
|
1608
1774
|
if (!chosen) return `\u041D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0443\u0441\u043B\u043E\u0432\u0438\u044F: \u043D\u043E\u043C\u0435\u0440 ${exit}. \u041D\u043E\u043C\u0435\u0440\u0430 \u0443\u0441\u043B\u043E\u0432\u0438\u0439 \u2014 \u0432 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F.`;
|
|
1775
|
+
if (runtime.confirmations?.guardFor(turn.pinned.flowId, turn.run.state, chosen.exitId)) return "\u042D\u0442\u043E\u0442 \u0432\u044B\u0445\u043E\u0434 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442\u0441\u044F \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C \u043F\u043E\u0441\u043B\u0435 \u043F\u043E\u043B\u043D\u043E\u0433\u043E \u043E\u0442\u0432\u0435\u0442\u0430: \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043D\u043D\u0430\u044F \u0432\u0435\u0445\u0430 \u0438 \u0430\u043A\u0442\u0443\u0430\u043B\u044C\u043D\u043E\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D\u044B \u0432\u043C\u0435\u0441\u0442\u0435. \u041F\u0435\u0440\u0435\u0445\u043E\u0434 \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D.";
|
|
1609
1776
|
if (!claimAllowed(turn.run, chosen.exitId)) return "\u042D\u0442\u043E \u0443\u0441\u043B\u043E\u0432\u0438\u0435 \u0443\u0436\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u043B\u0438: \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0438 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440.";
|
|
1610
1777
|
const state = stateById(flow, turn.run.state);
|
|
1611
1778
|
let verdict;
|
|
@@ -1887,6 +2054,7 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
1887
2054
|
const now = () => clock().getTime();
|
|
1888
2055
|
const iso = () => clock().toISOString();
|
|
1889
2056
|
const logError = options.logError ?? ((message) => console.error(message));
|
|
2057
|
+
const confirmations = createConfirmations(options.confirmations, options.exitGuards);
|
|
1890
2058
|
const defaultTimeZone = options.defaultTimeZone === void 0 ? void 0 : ianaTimeZone(options.defaultTimeZone);
|
|
1891
2059
|
if (defaultTimeZone === null) throw new TypeError(`createAgentFlow: defaultTimeZone \xAB${options.defaultTimeZone}\xBB \u2014 \u043D\u0435 \u043F\u043E\u044F\u0441 IANA`);
|
|
1892
2060
|
let port;
|
|
@@ -2014,7 +2182,10 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
2014
2182
|
const key = `${flowId}\0${versionId}`;
|
|
2015
2183
|
let found = versions.get(key);
|
|
2016
2184
|
if (!found) {
|
|
2017
|
-
found = store.getVersion(flowId, versionId).then((saved) =>
|
|
2185
|
+
found = store.getVersion(flowId, versionId).then((saved) => {
|
|
2186
|
+
if (saved) confirmations.validate(flowId, saved.flow);
|
|
2187
|
+
return saved?.flow ?? null;
|
|
2188
|
+
});
|
|
2018
2189
|
versions.set(key, found);
|
|
2019
2190
|
void found.then((flow) => flow ?? versions.delete(key), () => versions.delete(key));
|
|
2020
2191
|
}
|
|
@@ -2030,12 +2201,15 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
2030
2201
|
}
|
|
2031
2202
|
function checkedFlow(flowId, versionId, value) {
|
|
2032
2203
|
const checked = flowOrProblem(flowId, versionId, value);
|
|
2033
|
-
if ("flow" in checked)
|
|
2204
|
+
if ("flow" in checked) {
|
|
2205
|
+
confirmations.validate(flowId, checked.flow);
|
|
2206
|
+
return checked.flow;
|
|
2207
|
+
}
|
|
2034
2208
|
logError(checked.problem);
|
|
2035
2209
|
return null;
|
|
2036
2210
|
}
|
|
2037
2211
|
async function saveChecked(loaded, flow) {
|
|
2038
|
-
const saved = await store.saveVersion({ flowId: loaded.flowId, versionId: loaded.versionId, hash:
|
|
2212
|
+
const saved = await store.saveVersion({ flowId: loaded.flowId, versionId: loaded.versionId, hash: createHash2("sha256").update(stableJson(flow)).digest("hex"), flow, at: iso() });
|
|
2039
2213
|
if (saved === "conflict") logError(`[\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439] ${loaded.flowId}@${loaded.versionId}: \u043F\u043E\u0434 \u044D\u0442\u0438\u043C \u043D\u043E\u043C\u0435\u0440\u043E\u043C \u0443\u0436\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u0434\u0440\u0443\u0433\u043E\u0439 \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439, \u0447\u0430\u0442 \u0438\u0434\u0451\u0442 \u043F\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u043E\u043C\u0443`);
|
|
2040
2214
|
return saved;
|
|
2041
2215
|
}
|
|
@@ -2156,6 +2330,7 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
2156
2330
|
return remember(chatId, await pinned);
|
|
2157
2331
|
}
|
|
2158
2332
|
const runtime = {
|
|
2333
|
+
confirmations,
|
|
2159
2334
|
store,
|
|
2160
2335
|
now,
|
|
2161
2336
|
logError,
|
|
@@ -2239,7 +2414,7 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
2239
2414
|
logError(`[\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439] \u0447\u0430\u0442=${record.chatId} \u0442\u0430\u0439\u043C\u0435\u0440 \u043D\u0435 \u043F\u0435\u0440\u0435\u0441\u0447\u0438\u0442\u0430\u043D: ${errorText5(error)}`);
|
|
2240
2415
|
}
|
|
2241
2416
|
}
|
|
2242
|
-
async function deliverFirstText(ownerId, chatId, messageId, condition, text) {
|
|
2417
|
+
async function deliverFirstText(ownerId, chatId, messageId, condition, text, guarded2 = false) {
|
|
2243
2418
|
for (let attempt = 0; ; attempt += 1) {
|
|
2244
2419
|
const result = await (await portOf({ ownerId, chatId })).deliverNotices({
|
|
2245
2420
|
ownerId,
|
|
@@ -2247,7 +2422,8 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
2247
2422
|
notices: [{ kind: "app", name: "flow_judge", title: `\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440\u0430: ${condition}` }],
|
|
2248
2423
|
wakeAgent: true,
|
|
2249
2424
|
deliveryKey: `flow:${chatId}:judge:${messageId}`,
|
|
2250
|
-
|
|
2425
|
+
// Guarded decision может быть отозван после judge: готовый reply обошёл бы результат prepareTurn.
|
|
2426
|
+
...!guarded2 && { reply: { text } }
|
|
2251
2427
|
});
|
|
2252
2428
|
if (result.status !== "busy") return;
|
|
2253
2429
|
if (attempt >= DELIVERY_RETRY_MS.length) return;
|
|
@@ -2258,7 +2434,8 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
2258
2434
|
const { ownerId, chatId, messageId } = result;
|
|
2259
2435
|
const pinned = await chatFlow(ownerId, chatId);
|
|
2260
2436
|
if (!pinned) return;
|
|
2261
|
-
const
|
|
2437
|
+
const service = await portOf({ ownerId, chatId });
|
|
2438
|
+
const { values, atMessageId } = await service.getChatState(ownerId, chatId);
|
|
2262
2439
|
const run = storedRun(values[FLOW_STATE_KEY]);
|
|
2263
2440
|
if (!run || run.answerId !== messageId) return;
|
|
2264
2441
|
await clearPlacement(chatId);
|
|
@@ -2269,7 +2446,30 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
2269
2446
|
const state = stateById(pinned.flow, run.state);
|
|
2270
2447
|
if (!state || isFinal(state)) return;
|
|
2271
2448
|
const conditions = judgeExits(pinned.flow, run.state);
|
|
2272
|
-
|
|
2449
|
+
const withConfirmations = confirmations.forState(pinned.flowId, run.state).length > 0 || conditions.some(({ exitId }) => confirmations.guardFor(pinned.flowId, run.state, exitId));
|
|
2450
|
+
if (conditions.length === 0 && !withConfirmations) return;
|
|
2451
|
+
if (withConfirmations) {
|
|
2452
|
+
const checked = await judgeConfirmations({
|
|
2453
|
+
confirmations,
|
|
2454
|
+
pinned,
|
|
2455
|
+
run,
|
|
2456
|
+
result,
|
|
2457
|
+
port: service,
|
|
2458
|
+
atMessageId,
|
|
2459
|
+
judge: judge(),
|
|
2460
|
+
threshold: options.judge.threshold ?? 0.7,
|
|
2461
|
+
now: chatTime(run, now()),
|
|
2462
|
+
conditions,
|
|
2463
|
+
goal: stateGoal(state.instructions),
|
|
2464
|
+
messages: await history(ownerId, chatId, { run, answer: { id: messageId, parts: result.parts } })
|
|
2465
|
+
});
|
|
2466
|
+
if (!checked.saved || !checked.transition) return;
|
|
2467
|
+
const transition2 = checked.transition;
|
|
2468
|
+
if (!confirmations.guardFor(pinned.flowId, run.state, transition2.exitId)) await store.addPending(chatId, { kind: "transition", key: `judge:${messageId}`, transition: transition2, branchHead: messageId });
|
|
2469
|
+
const first2 = exactFirstText(pinned.flow, transition2.to);
|
|
2470
|
+
if (first2) await deliverFirstText(ownerId, chatId, messageId, conditions.find((item) => item.exitId === transition2.exitId).text, first2, Boolean(confirmations.guardFor(pinned.flowId, run.state, transition2.exitId)));
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2273
2473
|
const verdicts = await judge().checkAll({
|
|
2274
2474
|
ownerId,
|
|
2275
2475
|
chatId,
|
|
@@ -2287,6 +2487,34 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
2287
2487
|
const first = exactFirstText(pinned.flow, transition.to);
|
|
2288
2488
|
if (first) await deliverFirstText(ownerId, chatId, messageId, conditions.find((item) => item.exitId === met.exitId).text, first);
|
|
2289
2489
|
}
|
|
2490
|
+
async function revokeConfirmation(input) {
|
|
2491
|
+
if (!input.reason?.trim() || input.reason.length > 1e3) throw new TypeError("revokeConfirmation: reason \u2014 \u043D\u0435\u043F\u0443\u0441\u0442\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0434\u043E 1000 \u0437\u043D\u0430\u043A\u043E\u0432");
|
|
2492
|
+
if (typeof input.idempotencyKey !== "string" || !input.idempotencyKey.trim() || input.idempotencyKey.length > 180) throw new TypeError("revokeConfirmation: idempotencyKey \u2014 \u043D\u0435\u043F\u0443\u0441\u0442\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0434\u043E 180 \u0437\u043D\u0430\u043A\u043E\u0432");
|
|
2493
|
+
const pinned = await chatFlow(input.ownerId, input.chatId);
|
|
2494
|
+
if (!pinned || pinned.flowId !== input.flowId) return { status: "not_found" };
|
|
2495
|
+
const spec = confirmations.specFor(input.flowId, input);
|
|
2496
|
+
if (!spec) return { status: "stale" };
|
|
2497
|
+
const service = await portOf(input);
|
|
2498
|
+
const commit2 = flowStateCommitter2(service);
|
|
2499
|
+
if (!commit2) throw new Error("confirmations: \u0441\u043B\u0443\u0436\u0431\u0430 \u0447\u0430\u0442\u0430 \u043D\u0435 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u044E\u044E flows capability");
|
|
2500
|
+
const snapshot = await service.getChatState(input.ownerId, input.chatId);
|
|
2501
|
+
const run = storedRun(snapshot.values[FLOW_STATE_KEY]);
|
|
2502
|
+
if (!run) return { status: "not_found" };
|
|
2503
|
+
const old = run.confirmations?.[spec.key];
|
|
2504
|
+
if (old && (old.scope?.ownerId !== input.ownerId || old.scope.chatId !== input.chatId || old.scope.flowId !== input.flowId)) return { status: "stale" };
|
|
2505
|
+
if (old?.status === "revoked" && old.operationKey === input.idempotencyKey) {
|
|
2506
|
+
if (old.reason !== input.reason || old.revision !== input.revision) throw new TypeError("revokeConfirmation: idempotencyKey \u0443\u0436\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D \u0441 \u0434\u0440\u0443\u0433\u0438\u043C payload");
|
|
2507
|
+
return { status: "same", atMessageId: snapshot.atMessageId };
|
|
2508
|
+
}
|
|
2509
|
+
if (snapshot.atMessageId !== input.expectedAtMessageId || old && (old.revision !== spec.revision || old.criterionHash !== confirmationHash(spec))) return { status: "stale" };
|
|
2510
|
+
const scope = { ownerId: input.ownerId, chatId: input.chatId, flowId: input.flowId };
|
|
2511
|
+
const { checkedTransition, ...withoutDecision } = run;
|
|
2512
|
+
const next = { ...withoutDecision, ...checkedTransition && checkedTransition.requires.key !== spec.key && { checkedTransition }, confirmations: {
|
|
2513
|
+
...run.confirmations,
|
|
2514
|
+
[spec.key]: { key: spec.key, revision: spec.revision, status: "revoked", versionId: pinned.versionId, scope, criterionHash: confirmationHash(spec), revokedAt: chatTime(run, now()), afterMessageId: run.answerId ?? null, reason: input.reason, operationKey: input.idempotencyKey }
|
|
2515
|
+
} };
|
|
2516
|
+
return commit2({ ...scope, revision: confirmations.revision(input.flowId), expectedAtMessageId: input.expectedAtMessageId, idempotencyKey: `revoke:${input.idempotencyKey}`, flow: next });
|
|
2517
|
+
}
|
|
2290
2518
|
async function buttonPressed(context) {
|
|
2291
2519
|
const pinned = await chatFlow(context.ownerId, context.chatId);
|
|
2292
2520
|
if (!pinned) return;
|
|
@@ -2418,7 +2646,13 @@ function buildAgentFlow(options, modules = {}) {
|
|
|
2418
2646
|
const transitions = [];
|
|
2419
2647
|
const input = { now: at, card: await runtime.cardValues(ownerId), out: transitions, onChainLimit: () => {
|
|
2420
2648
|
}, timeZone };
|
|
2421
|
-
|
|
2649
|
+
const decision = run.checkedTransition;
|
|
2650
|
+
if (decision && decision.versionId === pinned.versionId && decision.answerId === run.answerId && decision.transition.from === run.state && confirmations.allows({ ownerId, chatId, flowId: pinned.flowId }, run, decision.transition.exitId)) {
|
|
2651
|
+
const { checkedTransition, ...withoutDecision } = run;
|
|
2652
|
+
run = advance(pinned.flow, withoutDecision, pendingEvent(decision.transition, timeZone), input);
|
|
2653
|
+
}
|
|
2654
|
+
const pending = (record?.pending ?? []).filter((change) => change.kind !== "transition" || !confirmations.guardFor(pinned.flowId, change.transition.from, change.transition.exitId));
|
|
2655
|
+
run = withPending(pinned.flow, run, pending, input);
|
|
2422
2656
|
const schedule = pinned.flow.schedule;
|
|
2423
2657
|
if (record?.endedAt) run = endedRun(pinned.flow, run, { endedAt: Date.parse(record.endedAt), endReason: record.endReason ?? "final" });
|
|
2424
2658
|
const { answerId: _answerId, firstReasons: _firstReasons, ...plain } = run;
|
|
@@ -2510,7 +2744,7 @@ ${rules}`;
|
|
|
2510
2744
|
} catch (error) {
|
|
2511
2745
|
logError(`[\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439] \u0447\u0430\u0442=${input.chatId} \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435: ${errorText5(error)}`);
|
|
2512
2746
|
}
|
|
2513
|
-
await reschedule(input.ownerId, input.chatId, run, input.atMessageId);
|
|
2747
|
+
await reschedule(input.ownerId, input.chatId, run, input.reason === "flowCommit" ? run.answerId ?? null : input.atMessageId);
|
|
2514
2748
|
})();
|
|
2515
2749
|
track(moved);
|
|
2516
2750
|
await moved;
|
|
@@ -2549,10 +2783,14 @@ ${rules}`;
|
|
|
2549
2783
|
noteStaffReply: (input) => pauses().noteStaffReply(input),
|
|
2550
2784
|
reportDelivery: (input) => delivery().report(input),
|
|
2551
2785
|
readRun,
|
|
2786
|
+
revokeConfirmation,
|
|
2787
|
+
previewMigration: (input) => previewConfirmedMigration(input, { store, confirmations, portFor: portOf }),
|
|
2552
2788
|
chatsByState: (flowId, options2) => migrator().chatsByState(flowId, options2),
|
|
2553
2789
|
async migrateChats(input) {
|
|
2554
2790
|
try {
|
|
2555
|
-
return await migrator().migrateChats(input);
|
|
2791
|
+
if (!input.requiresConfirmation) return await migrator().migrateChats(input);
|
|
2792
|
+
const preview = await previewConfirmedMigration({ ...input, requiresConfirmation: input.requiresConfirmation }, { store, confirmations, portFor: portOf });
|
|
2793
|
+
return await migrator().migrateChats({ ...input, chatIds: preview.eligible, filter: void 0 });
|
|
2556
2794
|
} finally {
|
|
2557
2795
|
forgetPorts();
|
|
2558
2796
|
}
|