@rei-standard/amsg-server 2.6.0-next.7 → 2.6.0-next.9
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/README.md +45 -0
- package/dist/{chunk-YQPWXPIN.cjs → chunk-3QRVP4JE.cjs} +147 -7
- package/dist/{chunk-I7EUMYQ4.mjs → chunk-S6O3MXQO.mjs} +147 -7
- package/dist/{cloudflare-BBPmRS2L.d.cts → cloudflare-kUY6z-k5.d.cts} +273 -14
- package/dist/{cloudflare-BBPmRS2L.d.ts → cloudflare-kUY6z-k5.d.ts} +273 -14
- package/dist/cloudflare.cjs +2 -2
- package/dist/cloudflare.d.cts +1 -1
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.mjs +1 -1
- package/dist/index.cjs +11 -11
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -777,6 +777,9 @@ async function callLlm(payload, options = {}) {
|
|
|
777
777
|
* - include it only when payload.maxTokens is provided
|
|
778
778
|
* - omit it when payload.maxTokens is undefined / null
|
|
779
779
|
*
|
|
780
|
+
* `tools` / `tool_choice` are optional as well: forwarded only when the
|
|
781
|
+
* caller passes a non-empty payload.tools.
|
|
782
|
+
*
|
|
780
783
|
* @param {Object} payload
|
|
781
784
|
* @returns {Object}
|
|
782
785
|
*/
|
|
@@ -804,6 +807,18 @@ function buildAiRequestBody(payload) {
|
|
|
804
807
|
requestBody.temperature = 0.8;
|
|
805
808
|
}
|
|
806
809
|
|
|
810
|
+
// tools mode (added in v2.6.0): forward the caller's OpenAI tools array
|
|
811
|
+
// verbatim — same philosophy as messages mode above. The agentic loop
|
|
812
|
+
// already appends assistant tool_calls + role:'tool' results; this is
|
|
813
|
+
// the request-side half of the same protocol. An empty array is treated
|
|
814
|
+
// as "no tools" because some OpenAI-compatible relays reject `tools: []`.
|
|
815
|
+
if (Array.isArray(payload.tools) && payload.tools.length > 0) {
|
|
816
|
+
requestBody.tools = payload.tools;
|
|
817
|
+
if (payload.toolChoice !== undefined && payload.toolChoice !== null) {
|
|
818
|
+
requestBody.tool_choice = payload.toolChoice;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
807
822
|
if (payload.maxTokens === undefined || payload.maxTokens === null) {
|
|
808
823
|
return requestBody;
|
|
809
824
|
}
|
|
@@ -1163,6 +1178,37 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
1163
1178
|
* 不用再自己维护 Map<sessionId, state>。fire 结束(finish / skip-push /
|
|
1164
1179
|
* 抛错 / 轮数超限)后随调用栈丢弃;库不读不写、不落库、不打日志。
|
|
1165
1180
|
*
|
|
1181
|
+
* 工具声明:onBeforeFire 的返回值里可以带 `tools`(OpenAI 的 tools 数组,
|
|
1182
|
+
* 另有可选的 `toolChoice`),本次 fire 的每一轮 LLM 请求都原样带上它们——
|
|
1183
|
+
* 补完那轮模型仍可能再发起调用。库不看 tools 的内容,执行照旧是
|
|
1184
|
+
* executeToolCalls 的事。
|
|
1185
|
+
*
|
|
1186
|
+
* 自排后续任务:fireCtx 和每轮的 sessionCtx 上都挂着 scheduleTask,宿主用它
|
|
1187
|
+
* 在这次 fire 里给同一个用户再建一条定时任务(「这条发完,一个半小时后我再
|
|
1188
|
+
* 接着说一句」)。写口在 sessionCtx 上也给,是因为要不要接着说往往是看完
|
|
1189
|
+
* LLM 输出才定的,那时 onBeforeFire 早已返回。凭据(pushSubscription /
|
|
1190
|
+
* apiKey)和投递配置从当前任务继承、宿主全程看不到(与 buildHookTask 屏蔽
|
|
1191
|
+
* 凭据同一个原则),宿主只提供「什么时候、说什么方向」。
|
|
1192
|
+
*
|
|
1193
|
+
* 新任务不继承 completePrompt / messages,两者都置 null:fire-time hook 每次
|
|
1194
|
+
* 现场重组 prompt,把排程时冻结的旧 prompt 带过去,会在新任务万一走回老链路
|
|
1195
|
+
* (onBeforeFire 返回 null、或那次部署没配 hooks)时静默顶替宿主的意图——
|
|
1196
|
+
* 与其发一条来路不明的旧文案,不如让它当场失败、走既有的重试/标记逻辑。
|
|
1197
|
+
*
|
|
1198
|
+
* 护栏(几条都是这个能力能不能上线的关键):
|
|
1199
|
+
* - firstSendTime 至少比现在晚 MIN_SCHEDULE_LEAD_MS(60 秒)。cron 每分钟
|
|
1200
|
+
* 一跳,排在 60 秒内等于让下一跳立刻捡走,容易变成自己触发自己的紧密循环。
|
|
1201
|
+
* - messageType 只收 auto / prompted / fixed,不收 instant。instant 的语义是
|
|
1202
|
+
* 「建行的那一刻就投递」,那条路径归 POST /schedule-message 管;从 fire 里
|
|
1203
|
+
* 造这么一行,投递时机反而说不清(这里不投,要等 cron 捞到才发)。
|
|
1204
|
+
* - 单次 fire 的建任务上限默认 DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE 条,
|
|
1205
|
+
* factory 配置 ctx.maxScheduledTasksPerFire 可覆盖(0 表示不许自排)。模型
|
|
1206
|
+
* 自排后续本质上是个可以无限延伸的链:没有上限的话,一次 fire 里连排十条、
|
|
1207
|
+
* 或者每条任务都排下一条,就成了谁也没按下停止键的循环。
|
|
1208
|
+
* - uuid 冲突不抛错,返回 { created: false, reason: 'duplicate' }。fire 失败
|
|
1209
|
+
* 会整条重跑(run-tick 的重试语义),宿主传一个由「任务 id + 触发时刻」推
|
|
1210
|
+
* 出来的确定性 uuid 就天然幂等,不会每重试一次多排一条。
|
|
1211
|
+
*
|
|
1166
1212
|
* Budget guards, both factory-level (ctx.maxToolIterations /
|
|
1167
1213
|
* ctx.totalTimeoutMs) and per-fire (onBeforeFire may return
|
|
1168
1214
|
* { messages, maxToolIterations?, totalTimeoutMs? } to override for one
|
|
@@ -1177,6 +1223,17 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
1177
1223
|
const DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
1178
1224
|
const DEFAULT_TOTAL_TIMEOUT_MS = 240_000;
|
|
1179
1225
|
|
|
1226
|
+
// 一次 fire 里最多能给自己排几条后续任务(ctx.maxScheduledTasksPerFire 可覆盖)。
|
|
1227
|
+
const DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
|
|
1228
|
+
// 自排任务的最小提前量:cron 一分钟一跳,比这更近等于让下一跳立刻捡走。
|
|
1229
|
+
const MIN_SCHEDULE_LEAD_MS = 60_000;
|
|
1230
|
+
|
|
1231
|
+
// 自排任务允许的类型。instant 归 POST /schedule-message 那条同步路径管。
|
|
1232
|
+
const SCHEDULABLE_MESSAGE_TYPES = new Set(['auto', 'prompted', 'fixed']);
|
|
1233
|
+
// 与 validateScheduleMessagePayload 同一套;run-tick 只认得这三种,别的值会让
|
|
1234
|
+
// 任务发完之后推进不到下一次。
|
|
1235
|
+
const SCHEDULABLE_RECURRENCE_TYPES = new Set(['none', 'daily', 'weekly']);
|
|
1236
|
+
|
|
1180
1237
|
// Same pacing as the legacy path / amsg-instant.
|
|
1181
1238
|
const SLEEP_BETWEEN_MESSAGES_MS$1 = 1500;
|
|
1182
1239
|
|
|
@@ -1231,10 +1288,15 @@ function normalizeBeforeFireResult(result) {
|
|
|
1231
1288
|
messages: result.messages,
|
|
1232
1289
|
maxToolIterations: result.maxToolIterations,
|
|
1233
1290
|
totalTimeoutMs: result.totalTimeoutMs,
|
|
1291
|
+
// 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
|
|
1292
|
+
// 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
|
|
1293
|
+
// buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
|
|
1294
|
+
tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : undefined,
|
|
1295
|
+
toolChoice: result.toolChoice,
|
|
1234
1296
|
};
|
|
1235
1297
|
}
|
|
1236
1298
|
throw new TypeError(
|
|
1237
|
-
'AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs? } | { skip: true } | null'
|
|
1299
|
+
'AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs?, tools?, toolChoice? } | { skip: true } | null'
|
|
1238
1300
|
);
|
|
1239
1301
|
}
|
|
1240
1302
|
|
|
@@ -1259,7 +1321,7 @@ function firstPositiveNumber(values, fallback) {
|
|
|
1259
1321
|
* @param {import('../adapters/interface.js').TaskRow} args.task
|
|
1260
1322
|
* @param {Object} args.decryptedPayload - decrypted task payload (has credentials; they stop here)
|
|
1261
1323
|
* @param {string} args.userKey - per-user storage key (for readState decryption)
|
|
1262
|
-
* @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs })
|
|
1324
|
+
* @param {Object} args.ctx - processor ctx ({ db, webpush, vapid, hooks, maxToolIterations, totalTimeoutMs, maxScheduledTasksPerFire })
|
|
1263
1325
|
* @returns {Promise<{ handled: false } | { handled: true, result: { success: true, messagesSent: number, status: 'finished'|'skipped', iterations: number } }>}
|
|
1264
1326
|
* `handled: false` → caller falls back to the legacy frozen-prompt path
|
|
1265
1327
|
* (onBeforeFire returned null).
|
|
@@ -1378,6 +1440,174 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1378
1440
|
return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized });
|
|
1379
1441
|
};
|
|
1380
1442
|
|
|
1443
|
+
const maxScheduledTasksPerFire =
|
|
1444
|
+
Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0
|
|
1445
|
+
? ctx.maxScheduledTasksPerFire
|
|
1446
|
+
: DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
1447
|
+
// 本次 fire 已经用掉的建任务额度。校验通过、真的要写库时才 +1,参数不合法的
|
|
1448
|
+
// 调用不占额度;uuid 撞车(那条任务其实已经建出来了)照样占。
|
|
1449
|
+
let scheduledTaskCount = 0;
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* scheduleTask(options) —— 在这次 fire 里给**同一个用户**再建一条定时任务。
|
|
1453
|
+
*
|
|
1454
|
+
* 典型用法:角色发完这条,想过一个半小时再接着说一句。以前只能写进
|
|
1455
|
+
* client_state 等客户端上线重放,用户一直不上线这条链就断了;建成任务行之后,
|
|
1456
|
+
* 到点由 cron 直接触发,跟别的定时消息一样。
|
|
1457
|
+
*
|
|
1458
|
+
* 凭据与投递配置(pushSubscription / apiUrl / apiKey / primaryModel /
|
|
1459
|
+
* maxTokens / temperature / splitPattern)以及 contactName / avatarUrl /
|
|
1460
|
+
* messageSubtype / userMessage 从当前任务继承,宿主只说「什么时候、说什么
|
|
1461
|
+
* 方向」——hook 全程看不到凭据。completePrompt / messages 不继承(都置 null),
|
|
1462
|
+
* 见文件头。
|
|
1463
|
+
*
|
|
1464
|
+
* @param {Object} options
|
|
1465
|
+
* @param {string} options.firstSendTime - 必填,ISO 8601;至少比现在晚 60 秒
|
|
1466
|
+
* @param {'none'|'daily'|'weekly'} [options.recurrenceType='none']
|
|
1467
|
+
* @param {'auto'|'prompted'|'fixed'} [options.messageType] - 默认继承当前任务
|
|
1468
|
+
* @param {Object} [options.metadata] - 整体替换(不是深合并)当前任务的 metadata
|
|
1469
|
+
* @param {string} [options.contactName] - 默认继承
|
|
1470
|
+
* @param {string|null} [options.avatarUrl] - 默认继承
|
|
1471
|
+
* @param {string} [options.messageSubtype] - 默认继承
|
|
1472
|
+
* @param {string|null} [options.userMessage] - 默认继承;messageType 'fixed' 时必须有
|
|
1473
|
+
* @param {string} [options.uuid] - 默认 randomUUID();传确定性 uuid 可做重试幂等
|
|
1474
|
+
* @returns {Promise<{ created: true, id: number|null, uuid: string, nextSendAt: string }
|
|
1475
|
+
* | { created: false, reason: 'duplicate', uuid: string }>}
|
|
1476
|
+
*/
|
|
1477
|
+
const scheduleTask = async (options) => {
|
|
1478
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
1479
|
+
throw new TypeError('scheduleTask(options) 需要一个对象,至少包含 { firstSendTime }');
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// ---- 触发时刻 ----
|
|
1483
|
+
if (typeof options.firstSendTime !== 'string' || !options.firstSendTime.trim()) {
|
|
1484
|
+
throw new RangeError('scheduleTask: firstSendTime 必填,且必须是 ISO 8601 字符串');
|
|
1485
|
+
}
|
|
1486
|
+
const firstSendAt = new Date(options.firstSendTime);
|
|
1487
|
+
if (Number.isNaN(firstSendAt.getTime())) {
|
|
1488
|
+
throw new RangeError(`scheduleTask: firstSendTime 解析不出合法时间:${options.firstSendTime}`);
|
|
1489
|
+
}
|
|
1490
|
+
const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
|
|
1491
|
+
if (firstSendAt.getTime() < earliest) {
|
|
1492
|
+
throw new RangeError(
|
|
1493
|
+
`scheduleTask: firstSendTime 至少要比现在晚 ${MIN_SCHEDULE_LEAD_MS / 1000} 秒` +
|
|
1494
|
+
`(最早可排 ${new Date(earliest).toISOString()},收到 ${firstSendAt.toISOString()})` +
|
|
1495
|
+
'——cron 一分钟一跳,排得更近等于让下一跳立刻捡走'
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
const nextSendAt = firstSendAt.toISOString();
|
|
1499
|
+
|
|
1500
|
+
// ---- 类型 ----
|
|
1501
|
+
const inheritedType = options.messageType == null;
|
|
1502
|
+
const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
|
|
1503
|
+
if (messageType === 'instant') {
|
|
1504
|
+
throw new TypeError(
|
|
1505
|
+
"scheduleTask: messageType 不能是 'instant'——instant 的语义是「建行的那一刻就投递」," +
|
|
1506
|
+
'那条路径归 POST /schedule-message 管;从 fire 里建一条 instant,投递时机反而说不清。' +
|
|
1507
|
+
(inheritedType ? '(这个 instant 是从当前任务继承来的,显式传 auto / prompted / fixed 覆盖它。)' : '')
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
|
|
1511
|
+
throw new TypeError(
|
|
1512
|
+
`scheduleTask: messageType 只能是 auto / prompted / fixed,收到 ${JSON.stringify(messageType)}`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const recurrenceType = options.recurrenceType == null ? 'none' : options.recurrenceType;
|
|
1517
|
+
if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
|
|
1518
|
+
throw new TypeError(
|
|
1519
|
+
`scheduleTask: recurrenceType 只能是 none / daily / weekly,收到 ${JSON.stringify(recurrenceType)}`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// ---- 继承 + 覆盖 ----
|
|
1524
|
+
const contactName = options.contactName === undefined ? decryptedPayload.contactName : options.contactName;
|
|
1525
|
+
if (typeof contactName !== 'string' || !contactName.trim()) {
|
|
1526
|
+
throw new TypeError('scheduleTask: contactName 必须是非空字符串(默认继承当前任务)');
|
|
1527
|
+
}
|
|
1528
|
+
const userMessage = options.userMessage === undefined
|
|
1529
|
+
? (decryptedPayload.userMessage ?? null)
|
|
1530
|
+
: options.userMessage;
|
|
1531
|
+
if (messageType === 'fixed' && (typeof userMessage !== 'string' || !userMessage.trim())) {
|
|
1532
|
+
throw new TypeError(
|
|
1533
|
+
"scheduleTask: messageType 'fixed' 必须有 userMessage(自己传,或从当前任务继承到)" +
|
|
1534
|
+
'——固定文本任务没有正文,就是一条永远发空的任务'
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
const metadata = options.metadata === undefined ? (decryptedPayload.metadata || {}) : options.metadata;
|
|
1538
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
1539
|
+
throw new TypeError('scheduleTask: metadata 必须是普通对象(整体替换当前任务的 metadata,不做深合并)');
|
|
1540
|
+
}
|
|
1541
|
+
const uuid = options.uuid == null ? randomUUID() : options.uuid;
|
|
1542
|
+
if (typeof uuid !== 'string' || !uuid.trim()) {
|
|
1543
|
+
throw new TypeError('scheduleTask: uuid 必须是非空字符串');
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// ---- 额度 ----
|
|
1547
|
+
if (scheduledTaskCount >= maxScheduledTasksPerFire) {
|
|
1548
|
+
throw new RangeError(
|
|
1549
|
+
`scheduleTask: 单次 fire 最多建 ${maxScheduledTasksPerFire} 条任务` +
|
|
1550
|
+
`(factory 配置 maxScheduledTasksPerFire 可调),这是第 ${scheduledTaskCount + 1} 条`
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
// readState 在适配器不支持时降级成空数组;建任务不一样,静默成功会让宿主
|
|
1554
|
+
// 以为后续那条已经排上了,其实谁也不会触发它。
|
|
1555
|
+
if (!ctx.db || typeof ctx.db.createTask !== 'function') {
|
|
1556
|
+
throw new Error('AGENTIC_SCHEDULE_UNSUPPORTED: 当前数据库适配器不支持建任务(缺 createTask)');
|
|
1557
|
+
}
|
|
1558
|
+
scheduledTaskCount++;
|
|
1559
|
+
|
|
1560
|
+
// 字段构成与 POST /schedule-message 落库的那份保持一致,只是走库内部、不经 HTTP。
|
|
1561
|
+
const fullTaskData = {
|
|
1562
|
+
contactName,
|
|
1563
|
+
avatarUrl: options.avatarUrl === undefined ? (decryptedPayload.avatarUrl || null) : (options.avatarUrl || null),
|
|
1564
|
+
messageType,
|
|
1565
|
+
messageSubtype: (options.messageSubtype === undefined ? decryptedPayload.messageSubtype : options.messageSubtype) || 'chat',
|
|
1566
|
+
userMessage: userMessage || null,
|
|
1567
|
+
firstSendTime: nextSendAt,
|
|
1568
|
+
recurrenceType,
|
|
1569
|
+
apiUrl: decryptedPayload.apiUrl || null,
|
|
1570
|
+
apiKey: decryptedPayload.apiKey || null,
|
|
1571
|
+
primaryModel: decryptedPayload.primaryModel || null,
|
|
1572
|
+
// 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
|
|
1573
|
+
// 任务走回老链路时顶替宿主的意图。
|
|
1574
|
+
completePrompt: null,
|
|
1575
|
+
messages: null,
|
|
1576
|
+
maxTokens: decryptedPayload.maxTokens ?? null,
|
|
1577
|
+
temperature: decryptedPayload.temperature ?? null,
|
|
1578
|
+
splitPattern: decryptedPayload.splitPattern ?? null,
|
|
1579
|
+
pushSubscription: decryptedPayload.pushSubscription,
|
|
1580
|
+
metadata,
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
1584
|
+
|
|
1585
|
+
let created;
|
|
1586
|
+
try {
|
|
1587
|
+
created = await ctx.db.createTask({
|
|
1588
|
+
user_id: task.user_id,
|
|
1589
|
+
uuid,
|
|
1590
|
+
encrypted_payload: encryptedPayload,
|
|
1591
|
+
next_send_at: nextSendAt,
|
|
1592
|
+
message_type: messageType,
|
|
1593
|
+
});
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
// 撞 uuid 不算错:fire 失败会整条重跑,宿主用确定性 uuid(任务 id + 触发
|
|
1596
|
+
// 时刻推出来的那种)就天然幂等,重试不会多排一条。
|
|
1597
|
+
if (isUniqueViolation(error)) return { created: false, reason: 'duplicate', uuid };
|
|
1598
|
+
throw error;
|
|
1599
|
+
}
|
|
1600
|
+
if (!created) {
|
|
1601
|
+
throw new Error('AGENTIC_SCHEDULE_FAILED: createTask 没有返回新建的任务行');
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
created: true,
|
|
1605
|
+
id: created.id ?? null,
|
|
1606
|
+
uuid: created.uuid ?? uuid,
|
|
1607
|
+
nextSendAt: created.next_send_at ?? nextSendAt,
|
|
1608
|
+
};
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1381
1611
|
// 单次 fire 的宿主便签:onBeforeFire 的 fireCtx 和同一次 fire 每轮的
|
|
1382
1612
|
// sessionCtx(onLLMOutput / executeToolCalls)拿到同一个对象引用,fire 结束
|
|
1383
1613
|
// (finish / skip-push / 抛错 / 轮数超限)随调用栈丢弃。库自己不读不写、
|
|
@@ -1389,6 +1619,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1389
1619
|
userId: task.user_id,
|
|
1390
1620
|
readState,
|
|
1391
1621
|
writeState,
|
|
1622
|
+
scheduleTask,
|
|
1392
1623
|
now: new Date(nowFn()),
|
|
1393
1624
|
scratch,
|
|
1394
1625
|
});
|
|
@@ -1430,16 +1661,21 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1430
1661
|
// must not outlive totalTimeoutMs waiting for its own 300s abort.
|
|
1431
1662
|
const roundTimeoutMs = Math.max(1, Math.min(300_000, deadline - nowFn()));
|
|
1432
1663
|
const { response: llmResponse } = await callLlm(
|
|
1433
|
-
{
|
|
1664
|
+
{
|
|
1665
|
+
...decryptedPayload,
|
|
1666
|
+
messages,
|
|
1667
|
+
...(normalized.tools ? { tools: normalized.tools, toolChoice: normalized.toolChoice } : {}),
|
|
1668
|
+
},
|
|
1434
1669
|
{ requireContent: false, timeoutMs: roundTimeoutMs }
|
|
1435
1670
|
);
|
|
1436
1671
|
|
|
1437
1672
|
const assistantMessage = extractAssistantMessage(llmResponse);
|
|
1438
1673
|
messages = [...messages, assistantMessage];
|
|
1439
1674
|
|
|
1440
|
-
// 共享的 SessionContext(与 amsg-instant
|
|
1441
|
-
//
|
|
1442
|
-
// onLLMOutput / executeToolCalls 的位置,
|
|
1675
|
+
// 共享的 SessionContext(与 amsg-instant 同形状)之上,再挂两个状态访问器
|
|
1676
|
+
// 和 scheduleTask:大内容要不要旁路存、要不要给自己排条后续,往往到工具跑完、
|
|
1677
|
+
// 组 push 时才知道,而那正是 onLLMOutput / executeToolCalls 的位置,
|
|
1678
|
+
// onBeforeFire 早就返回了。
|
|
1443
1679
|
const sessionCtx = Object.freeze({
|
|
1444
1680
|
...buildSessionContext({
|
|
1445
1681
|
sessionId,
|
|
@@ -1454,6 +1690,7 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1454
1690
|
}),
|
|
1455
1691
|
readState,
|
|
1456
1692
|
writeState,
|
|
1693
|
+
scheduleTask,
|
|
1457
1694
|
});
|
|
1458
1695
|
|
|
1459
1696
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
@@ -1505,10 +1742,24 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
1505
1742
|
|
|
1506
1743
|
// Text-protocol classifiers synthesize toolCalls the raw assistant
|
|
1507
1744
|
// message doesn't carry; stamp them on so the appended role:'tool'
|
|
1508
|
-
// results stay valid for OpenAI-compatible APIs.
|
|
1509
|
-
|
|
1745
|
+
// results stay valid for OpenAI-compatible APIs. Merge rather than pick
|
|
1746
|
+
// one side: when a native tool_call and a synthesized one land in the
|
|
1747
|
+
// same round, both need their id on the assistant turn, or the half
|
|
1748
|
+
// that's missing leaves an orphan role:'tool' and a strict relay
|
|
1749
|
+
// rejects the next round.
|
|
1750
|
+
//
|
|
1751
|
+
// Contract for onLLMOutput: the toolCalls it returns must cover every
|
|
1752
|
+
// native tool_call the model declared. A native call left out still
|
|
1753
|
+
// gets declared on the assistant turn (it came from the model) but has
|
|
1754
|
+
// no matching role:'tool' result, and a strict relay rejects the next
|
|
1755
|
+
// round over the unanswered id. Drop a call on purpose → return a
|
|
1756
|
+
// result for it saying so, don't just omit it.
|
|
1757
|
+
const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
|
|
1758
|
+
const nativeIds = new Set(nativeCalls.map((tc) => tc && tc.id));
|
|
1759
|
+
const synthesized = toolCalls.filter((tc) => !nativeIds.has(tc && tc.id));
|
|
1760
|
+
const assistantWithTools = synthesized.length === 0
|
|
1510
1761
|
? assistantMessage
|
|
1511
|
-
: { ...assistantMessage, tool_calls:
|
|
1762
|
+
: { ...assistantMessage, tool_calls: [...nativeCalls, ...synthesized] };
|
|
1512
1763
|
messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
|
|
1513
1764
|
}
|
|
1514
1765
|
|
|
@@ -3539,6 +3790,8 @@ const SERVER_FEATURES = Object.freeze([
|
|
|
3539
3790
|
'agentic-hooks',
|
|
3540
3791
|
'agentic-scratch',
|
|
3541
3792
|
'agentic-write-state',
|
|
3793
|
+
'agentic-fire-tools',
|
|
3794
|
+
'agentic-schedule-task',
|
|
3542
3795
|
'vapid-public-key',
|
|
3543
3796
|
]);
|
|
3544
3797
|
|
|
@@ -3578,6 +3831,7 @@ function createCapabilitiesHandler(ctx) {
|
|
|
3578
3831
|
* @param {number} [config.maxToolIterations] - factory default LLM-round cap for the agentic loop (default 5).
|
|
3579
3832
|
* @param {number} [config.totalTimeoutMs] - factory default wall-time ceiling for the agentic loop (default 240000).
|
|
3580
3833
|
* @param {number} [config.maxStateValueBytes] - client_state 单条 value 的总上限(默认 5MB)。超过 200KB 的值由服务端透明分块存储(见 lib/state-chunks.js)。
|
|
3834
|
+
* @param {number} [config.maxScheduledTasksPerFire] - 一次 fire 里 hook 用 ctx.scheduleTask() 最多能建几条后续任务(默认 2,0 表示不许自排)。
|
|
3581
3835
|
* @returns {{ handlers: Object, ctx: Object }}
|
|
3582
3836
|
*/
|
|
3583
3837
|
|
|
@@ -3607,7 +3861,9 @@ function createSingleUserServer(config) {
|
|
|
3607
3861
|
hooks: config.hooks || null,
|
|
3608
3862
|
maxToolIterations: config.maxToolIterations,
|
|
3609
3863
|
totalTimeoutMs: config.totalTimeoutMs,
|
|
3610
|
-
maxStateValueBytes: config.maxStateValueBytes
|
|
3864
|
+
maxStateValueBytes: config.maxStateValueBytes,
|
|
3865
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
3866
|
+
maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
|
|
3611
3867
|
};
|
|
3612
3868
|
|
|
3613
3869
|
return {
|
|
@@ -4044,10 +4300,11 @@ function createWebCryptoWebPush(vapid = {}, { ttl = SCHEDULED_DEFAULT_TTL } = {}
|
|
|
4044
4300
|
* Access-Control-* on responses. With no `cors` the Worker stays same-origin.
|
|
4045
4301
|
*
|
|
4046
4302
|
* Fire-time hooks are opt-in too: pass `hooks: { onBeforeFire, onLLMOutput,
|
|
4047
|
-
* executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs`
|
|
4048
|
-
* config to let scheduled AI tasks assemble
|
|
4049
|
-
* tool loop at fire time. Omit them and AI
|
|
4050
|
-
* prompt exactly as before. See
|
|
4303
|
+
* executeToolCalls }` (+ optional `maxToolIterations` / `totalTimeoutMs` /
|
|
4304
|
+
* `maxScheduledTasksPerFire`) in the config to let scheduled AI tasks assemble
|
|
4305
|
+
* their prompt and run a server-side tool loop at fire time. Omit them and AI
|
|
4306
|
+
* tasks replay the schedule-time frozen prompt exactly as before. See
|
|
4307
|
+
* lib/agentic-fire.js.
|
|
4051
4308
|
*
|
|
4052
4309
|
* scheduled() 每次触发都会先给任务占位(在行的 lease_until 上写租约),同一
|
|
4053
4310
|
* 条任务不会被相邻两跳重复触发(见 lib/run-tick.js)。租期默认 10 分钟,可以
|
|
@@ -4187,6 +4444,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
4187
4444
|
totalTimeoutMs: cfg.totalTimeoutMs,
|
|
4188
4445
|
// hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
|
|
4189
4446
|
maxStateValueBytes: cfg.maxStateValueBytes,
|
|
4447
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
4448
|
+
maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
|
|
4190
4449
|
// 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
|
|
4191
4450
|
claimLeaseMs: cfg.claimLeaseMs
|
|
4192
4451
|
});
|