@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
package/README.md
CHANGED
|
@@ -121,6 +121,51 @@ const { bytes, remainingBytes, withinLimit } = measurePushPayload(JSON.stringify
|
|
|
121
121
|
|
|
122
122
|
装不下的内容(长文、附件详情)建议走旁路:正文存进 `client_state`,push 里只带一个引用键,客户端上线后用 `GET /client-state` 取回。单用户 Worker 的 fire-time hook 用 `ctx.writeState()` 写,见 [`examples/cloudflare-single-user/README.md`](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/server/examples/cloudflare-single-user/README.md)。
|
|
123
123
|
|
|
124
|
+
## Fire 时刻 hooks
|
|
125
|
+
|
|
126
|
+
配上 `hooks: { onBeforeFire, onLLMOutput, executeToolCalls }` 之后,AI 类任务的 prompt 不再是排程那一刻冻结的文本,而是 cron 触发时现场组装,工具也在服务端就地跑完,全程不需要客户端在线。完整用法见 [`examples/cloudflare-single-user/README.md`](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/server/examples/cloudflare-single-user/README.md) 的「Fire 时刻 hooks」。
|
|
127
|
+
|
|
128
|
+
三个 hook 拿到的 ctx 上都有这几个口子:
|
|
129
|
+
|
|
130
|
+
| ctx 上的口子 | 干什么 |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `readState(ns)` / `writeState(ns, entries)` | 读写 `client_state`,和客户端 `GET/PUT /client-state` 是同一份数据 |
|
|
133
|
+
| `scheduleTask(options)` | 给同一个用户再建一条定时任务 |
|
|
134
|
+
| `scratch` | 本次 fire 的便签对象,三个 hook 共享同一个引用,fire 结束即丢弃 |
|
|
135
|
+
|
|
136
|
+
### `ctx.scheduleTask(options)`
|
|
137
|
+
|
|
138
|
+
角色在这次 fire 里给自己排一条后续任务:「这条发完,一个半小时后我再接着说一句」。建出来的是一条正常的任务行,到点由 cron 触发,用户全程离线也不影响。
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
const result = await ctx.scheduleTask({
|
|
142
|
+
firstSendTime: new Date(Date.now() + 90 * 60_000).toISOString(), // 必填,ISO 字符串
|
|
143
|
+
messageType: 'auto', // 可选,默认继承当前任务
|
|
144
|
+
recurrenceType: 'none', // 可选,默认 none
|
|
145
|
+
metadata: { beat: 'followup' }, // 可选,整体替换当前任务的 metadata(不深合并)
|
|
146
|
+
uuid: `fire-${ctx.task.id}-${ctx.task.nextSendAt}`, // 可选,默认随机
|
|
147
|
+
});
|
|
148
|
+
// → { created: true, id, uuid, nextSendAt }
|
|
149
|
+
// 或 { created: false, reason: 'duplicate', uuid }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
凭据和投递配置(`pushSubscription` / `apiUrl` / `apiKey` / `primaryModel` / `maxTokens` / `temperature` / `splitPattern`)以及 `contactName` / `avatarUrl` / `messageSubtype` / `userMessage` 从当前任务继承,宿主只说「什么时候、说什么方向」——hook 全程看不到凭据。`completePrompt` / `messages` 不继承(都置 `null`):hook 每次现场重组 prompt,把排程时冻结的旧 prompt 带过去,新任务万一走回冻结 prompt 老链路就会静默发出一条谁也没打算发的文案。
|
|
153
|
+
|
|
154
|
+
护栏:
|
|
155
|
+
|
|
156
|
+
| 护栏 | 阈值 / 规则 | 不满足时 | 为什么 |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
| `firstSendTime` | 必填、能解析成合法时间、至少比现在晚 **60 秒** | `RangeError` | cron 一分钟一跳,排在 60 秒内等于让下一跳立刻捡走,容易变成自己触发自己的紧密循环 |
|
|
159
|
+
| `messageType` | 只收 `auto` / `prompted` / `fixed` | `TypeError` | `instant` 的语义是「建行的那一刻就投递」,那条路径归 `POST /schedule-message` 管;从 fire 里造这么一行,投递时机反而说不清 |
|
|
160
|
+
| `messageType: 'fixed'` | 必须有 `userMessage`(自己传或继承到) | `TypeError` | 固定文本任务没有正文,就是一条永远发空的任务 |
|
|
161
|
+
| 单次 fire 的建任务条数 | 默认 **2 条**,factory 配置 `maxScheduledTasksPerFire` 可调(`0` = 不许自排) | `RangeError` | 模型自排后续本质上是条能无限延伸的链,没有上限就没人按停止键 |
|
|
162
|
+
| `uuid` 撞车 | 不当错误处理 | 返回 `{ created: false, reason: 'duplicate', uuid }` | fire 失败会整条重跑,宿主传一个由「任务 id + 触发时刻」推出来的确定性 uuid 就天然幂等 |
|
|
163
|
+
| 数据库适配器没有 `createTask` | — | 抛 `AGENTIC_SCHEDULE_UNSUPPORTED` | 静默成功会让宿主以为后续那条排上了,其实谁也不会触发它 |
|
|
164
|
+
|
|
165
|
+
`recurrenceType` 沿用排程接口那套 `none` / `daily` / `weekly`,别的值抛 `TypeError`。参数不合法的调用不占建任务额度;uuid 撞车占(那条任务其实已经建出来了)。
|
|
166
|
+
|
|
167
|
+
`GET /capabilities` 的 features 里有 `agentic-schedule-task`,前端可以据此判断部署的 worker 认不认这条链路。
|
|
168
|
+
|
|
124
169
|
## 导出(新增)
|
|
125
170
|
|
|
126
171
|
- `validateLlmMessagesArray(messages)` — 同步预校验 messages 数组,返回 `string | null`(错误信息 / 通过)。和 `@rei-standard/amsg-instant` 的校验规则字节级一致。
|
|
@@ -443,6 +443,12 @@ function buildAiRequestBody(payload) {
|
|
|
443
443
|
} else if (!Array.isArray(payload.messages)) {
|
|
444
444
|
requestBody.temperature = 0.8;
|
|
445
445
|
}
|
|
446
|
+
if (Array.isArray(payload.tools) && payload.tools.length > 0) {
|
|
447
|
+
requestBody.tools = payload.tools;
|
|
448
|
+
if (payload.toolChoice !== void 0 && payload.toolChoice !== null) {
|
|
449
|
+
requestBody.tool_choice = payload.toolChoice;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
446
452
|
if (payload.maxTokens === void 0 || payload.maxTokens === null) {
|
|
447
453
|
return requestBody;
|
|
448
454
|
}
|
|
@@ -635,6 +641,10 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
635
641
|
// src/server/lib/agentic-fire.js
|
|
636
642
|
var DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
637
643
|
var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
|
|
644
|
+
var DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
|
|
645
|
+
var MIN_SCHEDULE_LEAD_MS = 6e4;
|
|
646
|
+
var SCHEDULABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set(["auto", "prompted", "fixed"]);
|
|
647
|
+
var SCHEDULABLE_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["none", "daily", "weekly"]);
|
|
638
648
|
var SLEEP_BETWEEN_MESSAGES_MS = 1500;
|
|
639
649
|
var CREDENTIAL_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["apiKey", "pushSubscription"]);
|
|
640
650
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -667,11 +677,16 @@ function normalizeBeforeFireResult(result) {
|
|
|
667
677
|
return {
|
|
668
678
|
messages: result.messages,
|
|
669
679
|
maxToolIterations: result.maxToolIterations,
|
|
670
|
-
totalTimeoutMs: result.totalTimeoutMs
|
|
680
|
+
totalTimeoutMs: result.totalTimeoutMs,
|
|
681
|
+
// 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
|
|
682
|
+
// 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
|
|
683
|
+
// buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
|
|
684
|
+
tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : void 0,
|
|
685
|
+
toolChoice: result.toolChoice
|
|
671
686
|
};
|
|
672
687
|
}
|
|
673
688
|
throw new TypeError(
|
|
674
|
-
"AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs? } | { skip: true } | null"
|
|
689
|
+
"AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs?, tools?, toolChoice? } | { skip: true } | null"
|
|
675
690
|
);
|
|
676
691
|
}
|
|
677
692
|
function firstPositiveInt(values, fallback) {
|
|
@@ -757,12 +772,123 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
757
772
|
});
|
|
758
773
|
return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized2 });
|
|
759
774
|
};
|
|
775
|
+
const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
776
|
+
let scheduledTaskCount = 0;
|
|
777
|
+
const scheduleTask = async (options) => {
|
|
778
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
779
|
+
throw new TypeError("scheduleTask(options) \u9700\u8981\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u81F3\u5C11\u5305\u542B { firstSendTime }");
|
|
780
|
+
}
|
|
781
|
+
if (typeof options.firstSendTime !== "string" || !options.firstSendTime.trim()) {
|
|
782
|
+
throw new RangeError("scheduleTask: firstSendTime \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F ISO 8601 \u5B57\u7B26\u4E32");
|
|
783
|
+
}
|
|
784
|
+
const firstSendAt = new Date(options.firstSendTime);
|
|
785
|
+
if (Number.isNaN(firstSendAt.getTime())) {
|
|
786
|
+
throw new RangeError(`scheduleTask: firstSendTime \u89E3\u6790\u4E0D\u51FA\u5408\u6CD5\u65F6\u95F4\uFF1A${options.firstSendTime}`);
|
|
787
|
+
}
|
|
788
|
+
const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
|
|
789
|
+
if (firstSendAt.getTime() < earliest) {
|
|
790
|
+
throw new RangeError(
|
|
791
|
+
`scheduleTask: firstSendTime \u81F3\u5C11\u8981\u6BD4\u73B0\u5728\u665A ${MIN_SCHEDULE_LEAD_MS / 1e3} \u79D2\uFF08\u6700\u65E9\u53EF\u6392 ${new Date(earliest).toISOString()}\uFF0C\u6536\u5230 ${firstSendAt.toISOString()}\uFF09\u2014\u2014cron \u4E00\u5206\u949F\u4E00\u8DF3\uFF0C\u6392\u5F97\u66F4\u8FD1\u7B49\u4E8E\u8BA9\u4E0B\u4E00\u8DF3\u7ACB\u523B\u6361\u8D70`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const nextSendAt = firstSendAt.toISOString();
|
|
795
|
+
const inheritedType = options.messageType == null;
|
|
796
|
+
const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
|
|
797
|
+
if (messageType === "instant") {
|
|
798
|
+
throw new TypeError(
|
|
799
|
+
"scheduleTask: messageType \u4E0D\u80FD\u662F 'instant'\u2014\u2014instant \u7684\u8BED\u4E49\u662F\u300C\u5EFA\u884C\u7684\u90A3\u4E00\u523B\u5C31\u6295\u9012\u300D\uFF0C\u90A3\u6761\u8DEF\u5F84\u5F52 POST /schedule-message \u7BA1\uFF1B\u4ECE fire \u91CC\u5EFA\u4E00\u6761 instant\uFF0C\u6295\u9012\u65F6\u673A\u53CD\u800C\u8BF4\u4E0D\u6E05\u3002" + (inheritedType ? "\uFF08\u8FD9\u4E2A instant \u662F\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u6765\u7684\uFF0C\u663E\u5F0F\u4F20 auto / prompted / fixed \u8986\u76D6\u5B83\u3002\uFF09" : "")
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
|
|
803
|
+
throw new TypeError(
|
|
804
|
+
`scheduleTask: messageType \u53EA\u80FD\u662F auto / prompted / fixed\uFF0C\u6536\u5230 ${JSON.stringify(messageType)}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const recurrenceType = options.recurrenceType == null ? "none" : options.recurrenceType;
|
|
808
|
+
if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
|
|
809
|
+
throw new TypeError(
|
|
810
|
+
`scheduleTask: recurrenceType \u53EA\u80FD\u662F none / daily / weekly\uFF0C\u6536\u5230 ${JSON.stringify(recurrenceType)}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const contactName = options.contactName === void 0 ? decryptedPayload.contactName : options.contactName;
|
|
814
|
+
if (typeof contactName !== "string" || !contactName.trim()) {
|
|
815
|
+
throw new TypeError("scheduleTask: contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u9ED8\u8BA4\u7EE7\u627F\u5F53\u524D\u4EFB\u52A1\uFF09");
|
|
816
|
+
}
|
|
817
|
+
const userMessage = options.userMessage === void 0 ? _nullishCoalesce(decryptedPayload.userMessage, () => ( null)) : options.userMessage;
|
|
818
|
+
if (messageType === "fixed" && (typeof userMessage !== "string" || !userMessage.trim())) {
|
|
819
|
+
throw new TypeError(
|
|
820
|
+
"scheduleTask: messageType 'fixed' \u5FC5\u987B\u6709 userMessage\uFF08\u81EA\u5DF1\u4F20\uFF0C\u6216\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u5230\uFF09\u2014\u2014\u56FA\u5B9A\u6587\u672C\u4EFB\u52A1\u6CA1\u6709\u6B63\u6587\uFF0C\u5C31\u662F\u4E00\u6761\u6C38\u8FDC\u53D1\u7A7A\u7684\u4EFB\u52A1"
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const metadata = options.metadata === void 0 ? decryptedPayload.metadata || {} : options.metadata;
|
|
824
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
825
|
+
throw new TypeError("scheduleTask: metadata \u5FC5\u987B\u662F\u666E\u901A\u5BF9\u8C61\uFF08\u6574\u4F53\u66FF\u6362\u5F53\u524D\u4EFB\u52A1\u7684 metadata\uFF0C\u4E0D\u505A\u6DF1\u5408\u5E76\uFF09");
|
|
826
|
+
}
|
|
827
|
+
const uuid = options.uuid == null ? randomUUID() : options.uuid;
|
|
828
|
+
if (typeof uuid !== "string" || !uuid.trim()) {
|
|
829
|
+
throw new TypeError("scheduleTask: uuid \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
830
|
+
}
|
|
831
|
+
if (scheduledTaskCount >= maxScheduledTasksPerFire) {
|
|
832
|
+
throw new RangeError(
|
|
833
|
+
`scheduleTask: \u5355\u6B21 fire \u6700\u591A\u5EFA ${maxScheduledTasksPerFire} \u6761\u4EFB\u52A1\uFF08factory \u914D\u7F6E maxScheduledTasksPerFire \u53EF\u8C03\uFF09\uFF0C\u8FD9\u662F\u7B2C ${scheduledTaskCount + 1} \u6761`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (!ctx.db || typeof ctx.db.createTask !== "function") {
|
|
837
|
+
throw new Error("AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09");
|
|
838
|
+
}
|
|
839
|
+
scheduledTaskCount++;
|
|
840
|
+
const fullTaskData = {
|
|
841
|
+
contactName,
|
|
842
|
+
avatarUrl: options.avatarUrl === void 0 ? decryptedPayload.avatarUrl || null : options.avatarUrl || null,
|
|
843
|
+
messageType,
|
|
844
|
+
messageSubtype: (options.messageSubtype === void 0 ? decryptedPayload.messageSubtype : options.messageSubtype) || "chat",
|
|
845
|
+
userMessage: userMessage || null,
|
|
846
|
+
firstSendTime: nextSendAt,
|
|
847
|
+
recurrenceType,
|
|
848
|
+
apiUrl: decryptedPayload.apiUrl || null,
|
|
849
|
+
apiKey: decryptedPayload.apiKey || null,
|
|
850
|
+
primaryModel: decryptedPayload.primaryModel || null,
|
|
851
|
+
// 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
|
|
852
|
+
// 任务走回老链路时顶替宿主的意图。
|
|
853
|
+
completePrompt: null,
|
|
854
|
+
messages: null,
|
|
855
|
+
maxTokens: _nullishCoalesce(decryptedPayload.maxTokens, () => ( null)),
|
|
856
|
+
temperature: _nullishCoalesce(decryptedPayload.temperature, () => ( null)),
|
|
857
|
+
splitPattern: _nullishCoalesce(decryptedPayload.splitPattern, () => ( null)),
|
|
858
|
+
pushSubscription: decryptedPayload.pushSubscription,
|
|
859
|
+
metadata
|
|
860
|
+
};
|
|
861
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
862
|
+
let created;
|
|
863
|
+
try {
|
|
864
|
+
created = await ctx.db.createTask({
|
|
865
|
+
user_id: task.user_id,
|
|
866
|
+
uuid,
|
|
867
|
+
encrypted_payload: encryptedPayload,
|
|
868
|
+
next_send_at: nextSendAt,
|
|
869
|
+
message_type: messageType
|
|
870
|
+
});
|
|
871
|
+
} catch (error) {
|
|
872
|
+
if (isUniqueViolation(error)) return { created: false, reason: "duplicate", uuid };
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
if (!created) {
|
|
876
|
+
throw new Error("AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C");
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
created: true,
|
|
880
|
+
id: _nullishCoalesce(created.id, () => ( null)),
|
|
881
|
+
uuid: _nullishCoalesce(created.uuid, () => ( uuid)),
|
|
882
|
+
nextSendAt: _nullishCoalesce(created.next_send_at, () => ( nextSendAt))
|
|
883
|
+
};
|
|
884
|
+
};
|
|
760
885
|
const scratch = {};
|
|
761
886
|
const fireCtx = Object.freeze({
|
|
762
887
|
task: buildHookTask(task, decryptedPayload),
|
|
763
888
|
userId: task.user_id,
|
|
764
889
|
readState,
|
|
765
890
|
writeState,
|
|
891
|
+
scheduleTask,
|
|
766
892
|
now: new Date(nowFn()),
|
|
767
893
|
scratch
|
|
768
894
|
});
|
|
@@ -789,7 +915,11 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
789
915
|
}
|
|
790
916
|
const roundTimeoutMs = Math.max(1, Math.min(3e5, deadline - nowFn()));
|
|
791
917
|
const { response: llmResponse } = await callLlm(
|
|
792
|
-
{
|
|
918
|
+
{
|
|
919
|
+
...decryptedPayload,
|
|
920
|
+
messages,
|
|
921
|
+
...normalized.tools ? { tools: normalized.tools, toolChoice: normalized.toolChoice } : {}
|
|
922
|
+
},
|
|
793
923
|
{ requireContent: false, timeoutMs: roundTimeoutMs }
|
|
794
924
|
);
|
|
795
925
|
const assistantMessage = _amsgshared.extractAssistantMessage.call(void 0, llmResponse);
|
|
@@ -807,7 +937,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
807
937
|
scratch
|
|
808
938
|
}),
|
|
809
939
|
readState,
|
|
810
|
-
writeState
|
|
940
|
+
writeState,
|
|
941
|
+
scheduleTask
|
|
811
942
|
});
|
|
812
943
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
813
944
|
_amsgshared.assertValidDecision.call(void 0, decision, { inlineToolCalls: true });
|
|
@@ -845,7 +976,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
845
976
|
content: `Tool execution failed: ${_nullishCoalesce(_optionalChain([error, 'optionalAccess', _6 => _6.message]), () => ( String(error)))}`
|
|
846
977
|
}));
|
|
847
978
|
}
|
|
848
|
-
const
|
|
979
|
+
const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
|
|
980
|
+
const nativeIds = new Set(nativeCalls.map((tc) => tc && tc.id));
|
|
981
|
+
const synthesized = toolCalls.filter((tc) => !nativeIds.has(tc && tc.id));
|
|
982
|
+
const assistantWithTools = synthesized.length === 0 ? assistantMessage : { ...assistantMessage, tool_calls: [...nativeCalls, ...synthesized] };
|
|
849
983
|
messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
|
|
850
984
|
}
|
|
851
985
|
throw new Error(`AGENTIC_LOOP_EXCEEDED: no finish/skip-push decision within ${maxToolIterations} LLM round(s)`);
|
|
@@ -2262,7 +2396,7 @@ function createClientStateHandler(ctx) {
|
|
|
2262
2396
|
}
|
|
2263
2397
|
|
|
2264
2398
|
// src/server/lib/version.js
|
|
2265
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
2399
|
+
var SERVER_VERSION = true ? "2.6.0-next.9" : "0.0.0-dev";
|
|
2266
2400
|
|
|
2267
2401
|
// src/server/handlers/capabilities.js
|
|
2268
2402
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -2272,6 +2406,8 @@ var SERVER_FEATURES = Object.freeze([
|
|
|
2272
2406
|
"agentic-hooks",
|
|
2273
2407
|
"agentic-scratch",
|
|
2274
2408
|
"agentic-write-state",
|
|
2409
|
+
"agentic-fire-tools",
|
|
2410
|
+
"agentic-schedule-task",
|
|
2275
2411
|
"vapid-public-key"
|
|
2276
2412
|
]);
|
|
2277
2413
|
function createCapabilitiesHandler(ctx) {
|
|
@@ -2313,7 +2449,9 @@ function createSingleUserServer(config) {
|
|
|
2313
2449
|
hooks: config.hooks || null,
|
|
2314
2450
|
maxToolIterations: config.maxToolIterations,
|
|
2315
2451
|
totalTimeoutMs: config.totalTimeoutMs,
|
|
2316
|
-
maxStateValueBytes: config.maxStateValueBytes
|
|
2452
|
+
maxStateValueBytes: config.maxStateValueBytes,
|
|
2453
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
2454
|
+
maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
|
|
2317
2455
|
};
|
|
2318
2456
|
return {
|
|
2319
2457
|
ctx,
|
|
@@ -2652,6 +2790,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
2652
2790
|
totalTimeoutMs: cfg.totalTimeoutMs,
|
|
2653
2791
|
// hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
|
|
2654
2792
|
maxStateValueBytes: cfg.maxStateValueBytes,
|
|
2793
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
2794
|
+
maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
|
|
2655
2795
|
// 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
|
|
2656
2796
|
claimLeaseMs: cfg.claimLeaseMs
|
|
2657
2797
|
});
|
|
@@ -443,6 +443,12 @@ function buildAiRequestBody(payload) {
|
|
|
443
443
|
} else if (!Array.isArray(payload.messages)) {
|
|
444
444
|
requestBody.temperature = 0.8;
|
|
445
445
|
}
|
|
446
|
+
if (Array.isArray(payload.tools) && payload.tools.length > 0) {
|
|
447
|
+
requestBody.tools = payload.tools;
|
|
448
|
+
if (payload.toolChoice !== void 0 && payload.toolChoice !== null) {
|
|
449
|
+
requestBody.tool_choice = payload.toolChoice;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
446
452
|
if (payload.maxTokens === void 0 || payload.maxTokens === null) {
|
|
447
453
|
return requestBody;
|
|
448
454
|
}
|
|
@@ -635,6 +641,10 @@ async function writeClientStateEntries({ db, userId, userKey, entries }) {
|
|
|
635
641
|
// src/server/lib/agentic-fire.js
|
|
636
642
|
var DEFAULT_MAX_TOOL_ITERATIONS = 5;
|
|
637
643
|
var DEFAULT_TOTAL_TIMEOUT_MS = 24e4;
|
|
644
|
+
var DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE = 2;
|
|
645
|
+
var MIN_SCHEDULE_LEAD_MS = 6e4;
|
|
646
|
+
var SCHEDULABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set(["auto", "prompted", "fixed"]);
|
|
647
|
+
var SCHEDULABLE_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["none", "daily", "weekly"]);
|
|
638
648
|
var SLEEP_BETWEEN_MESSAGES_MS = 1500;
|
|
639
649
|
var CREDENTIAL_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["apiKey", "pushSubscription"]);
|
|
640
650
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -667,11 +677,16 @@ function normalizeBeforeFireResult(result) {
|
|
|
667
677
|
return {
|
|
668
678
|
messages: result.messages,
|
|
669
679
|
maxToolIterations: result.maxToolIterations,
|
|
670
|
-
totalTimeoutMs: result.totalTimeoutMs
|
|
680
|
+
totalTimeoutMs: result.totalTimeoutMs,
|
|
681
|
+
// 归一成「有就是非空数组,否则 undefined」,让下面每轮的透传判断能直接
|
|
682
|
+
// 用真值。真正决定请求体带不带 tools 的规则住在 llm.js 的
|
|
683
|
+
// buildAiRequestBody(那边有测试钉着),这层只是不把空数组往下传。
|
|
684
|
+
tools: Array.isArray(result.tools) && result.tools.length > 0 ? result.tools : void 0,
|
|
685
|
+
toolChoice: result.toolChoice
|
|
671
686
|
};
|
|
672
687
|
}
|
|
673
688
|
throw new TypeError(
|
|
674
|
-
"AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs? } | { skip: true } | null"
|
|
689
|
+
"AGENTIC_BAD_BEFORE_FIRE: onBeforeFire must return ChatMessage[] | { messages, maxToolIterations?, totalTimeoutMs?, tools?, toolChoice? } | { skip: true } | null"
|
|
675
690
|
);
|
|
676
691
|
}
|
|
677
692
|
function firstPositiveInt(values, fallback) {
|
|
@@ -757,12 +772,123 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
757
772
|
});
|
|
758
773
|
return writeClientStateEntries({ db: ctx.db, userId: task.user_id, userKey, entries: normalized2 });
|
|
759
774
|
};
|
|
775
|
+
const maxScheduledTasksPerFire = Number.isInteger(ctx.maxScheduledTasksPerFire) && ctx.maxScheduledTasksPerFire >= 0 ? ctx.maxScheduledTasksPerFire : DEFAULT_MAX_SCHEDULED_TASKS_PER_FIRE;
|
|
776
|
+
let scheduledTaskCount = 0;
|
|
777
|
+
const scheduleTask = async (options) => {
|
|
778
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
779
|
+
throw new TypeError("scheduleTask(options) \u9700\u8981\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u81F3\u5C11\u5305\u542B { firstSendTime }");
|
|
780
|
+
}
|
|
781
|
+
if (typeof options.firstSendTime !== "string" || !options.firstSendTime.trim()) {
|
|
782
|
+
throw new RangeError("scheduleTask: firstSendTime \u5FC5\u586B\uFF0C\u4E14\u5FC5\u987B\u662F ISO 8601 \u5B57\u7B26\u4E32");
|
|
783
|
+
}
|
|
784
|
+
const firstSendAt = new Date(options.firstSendTime);
|
|
785
|
+
if (Number.isNaN(firstSendAt.getTime())) {
|
|
786
|
+
throw new RangeError(`scheduleTask: firstSendTime \u89E3\u6790\u4E0D\u51FA\u5408\u6CD5\u65F6\u95F4\uFF1A${options.firstSendTime}`);
|
|
787
|
+
}
|
|
788
|
+
const earliest = nowFn() + MIN_SCHEDULE_LEAD_MS;
|
|
789
|
+
if (firstSendAt.getTime() < earliest) {
|
|
790
|
+
throw new RangeError(
|
|
791
|
+
`scheduleTask: firstSendTime \u81F3\u5C11\u8981\u6BD4\u73B0\u5728\u665A ${MIN_SCHEDULE_LEAD_MS / 1e3} \u79D2\uFF08\u6700\u65E9\u53EF\u6392 ${new Date(earliest).toISOString()}\uFF0C\u6536\u5230 ${firstSendAt.toISOString()}\uFF09\u2014\u2014cron \u4E00\u5206\u949F\u4E00\u8DF3\uFF0C\u6392\u5F97\u66F4\u8FD1\u7B49\u4E8E\u8BA9\u4E0B\u4E00\u8DF3\u7ACB\u523B\u6361\u8D70`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const nextSendAt = firstSendAt.toISOString();
|
|
795
|
+
const inheritedType = options.messageType == null;
|
|
796
|
+
const messageType = inheritedType ? decryptedPayload.messageType : options.messageType;
|
|
797
|
+
if (messageType === "instant") {
|
|
798
|
+
throw new TypeError(
|
|
799
|
+
"scheduleTask: messageType \u4E0D\u80FD\u662F 'instant'\u2014\u2014instant \u7684\u8BED\u4E49\u662F\u300C\u5EFA\u884C\u7684\u90A3\u4E00\u523B\u5C31\u6295\u9012\u300D\uFF0C\u90A3\u6761\u8DEF\u5F84\u5F52 POST /schedule-message \u7BA1\uFF1B\u4ECE fire \u91CC\u5EFA\u4E00\u6761 instant\uFF0C\u6295\u9012\u65F6\u673A\u53CD\u800C\u8BF4\u4E0D\u6E05\u3002" + (inheritedType ? "\uFF08\u8FD9\u4E2A instant \u662F\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u6765\u7684\uFF0C\u663E\u5F0F\u4F20 auto / prompted / fixed \u8986\u76D6\u5B83\u3002\uFF09" : "")
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (!SCHEDULABLE_MESSAGE_TYPES.has(messageType)) {
|
|
803
|
+
throw new TypeError(
|
|
804
|
+
`scheduleTask: messageType \u53EA\u80FD\u662F auto / prompted / fixed\uFF0C\u6536\u5230 ${JSON.stringify(messageType)}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const recurrenceType = options.recurrenceType == null ? "none" : options.recurrenceType;
|
|
808
|
+
if (!SCHEDULABLE_RECURRENCE_TYPES.has(recurrenceType)) {
|
|
809
|
+
throw new TypeError(
|
|
810
|
+
`scheduleTask: recurrenceType \u53EA\u80FD\u662F none / daily / weekly\uFF0C\u6536\u5230 ${JSON.stringify(recurrenceType)}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const contactName = options.contactName === void 0 ? decryptedPayload.contactName : options.contactName;
|
|
814
|
+
if (typeof contactName !== "string" || !contactName.trim()) {
|
|
815
|
+
throw new TypeError("scheduleTask: contactName \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u9ED8\u8BA4\u7EE7\u627F\u5F53\u524D\u4EFB\u52A1\uFF09");
|
|
816
|
+
}
|
|
817
|
+
const userMessage = options.userMessage === void 0 ? decryptedPayload.userMessage ?? null : options.userMessage;
|
|
818
|
+
if (messageType === "fixed" && (typeof userMessage !== "string" || !userMessage.trim())) {
|
|
819
|
+
throw new TypeError(
|
|
820
|
+
"scheduleTask: messageType 'fixed' \u5FC5\u987B\u6709 userMessage\uFF08\u81EA\u5DF1\u4F20\uFF0C\u6216\u4ECE\u5F53\u524D\u4EFB\u52A1\u7EE7\u627F\u5230\uFF09\u2014\u2014\u56FA\u5B9A\u6587\u672C\u4EFB\u52A1\u6CA1\u6709\u6B63\u6587\uFF0C\u5C31\u662F\u4E00\u6761\u6C38\u8FDC\u53D1\u7A7A\u7684\u4EFB\u52A1"
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const metadata = options.metadata === void 0 ? decryptedPayload.metadata || {} : options.metadata;
|
|
824
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
825
|
+
throw new TypeError("scheduleTask: metadata \u5FC5\u987B\u662F\u666E\u901A\u5BF9\u8C61\uFF08\u6574\u4F53\u66FF\u6362\u5F53\u524D\u4EFB\u52A1\u7684 metadata\uFF0C\u4E0D\u505A\u6DF1\u5408\u5E76\uFF09");
|
|
826
|
+
}
|
|
827
|
+
const uuid = options.uuid == null ? randomUUID() : options.uuid;
|
|
828
|
+
if (typeof uuid !== "string" || !uuid.trim()) {
|
|
829
|
+
throw new TypeError("scheduleTask: uuid \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
830
|
+
}
|
|
831
|
+
if (scheduledTaskCount >= maxScheduledTasksPerFire) {
|
|
832
|
+
throw new RangeError(
|
|
833
|
+
`scheduleTask: \u5355\u6B21 fire \u6700\u591A\u5EFA ${maxScheduledTasksPerFire} \u6761\u4EFB\u52A1\uFF08factory \u914D\u7F6E maxScheduledTasksPerFire \u53EF\u8C03\uFF09\uFF0C\u8FD9\u662F\u7B2C ${scheduledTaskCount + 1} \u6761`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (!ctx.db || typeof ctx.db.createTask !== "function") {
|
|
837
|
+
throw new Error("AGENTIC_SCHEDULE_UNSUPPORTED: \u5F53\u524D\u6570\u636E\u5E93\u9002\u914D\u5668\u4E0D\u652F\u6301\u5EFA\u4EFB\u52A1\uFF08\u7F3A createTask\uFF09");
|
|
838
|
+
}
|
|
839
|
+
scheduledTaskCount++;
|
|
840
|
+
const fullTaskData = {
|
|
841
|
+
contactName,
|
|
842
|
+
avatarUrl: options.avatarUrl === void 0 ? decryptedPayload.avatarUrl || null : options.avatarUrl || null,
|
|
843
|
+
messageType,
|
|
844
|
+
messageSubtype: (options.messageSubtype === void 0 ? decryptedPayload.messageSubtype : options.messageSubtype) || "chat",
|
|
845
|
+
userMessage: userMessage || null,
|
|
846
|
+
firstSendTime: nextSendAt,
|
|
847
|
+
recurrenceType,
|
|
848
|
+
apiUrl: decryptedPayload.apiUrl || null,
|
|
849
|
+
apiKey: decryptedPayload.apiKey || null,
|
|
850
|
+
primaryModel: decryptedPayload.primaryModel || null,
|
|
851
|
+
// 见文件头:fire-time hook 每次现场重组 prompt,旧 prompt 带过去只会在新
|
|
852
|
+
// 任务走回老链路时顶替宿主的意图。
|
|
853
|
+
completePrompt: null,
|
|
854
|
+
messages: null,
|
|
855
|
+
maxTokens: decryptedPayload.maxTokens ?? null,
|
|
856
|
+
temperature: decryptedPayload.temperature ?? null,
|
|
857
|
+
splitPattern: decryptedPayload.splitPattern ?? null,
|
|
858
|
+
pushSubscription: decryptedPayload.pushSubscription,
|
|
859
|
+
metadata
|
|
860
|
+
};
|
|
861
|
+
const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
|
|
862
|
+
let created;
|
|
863
|
+
try {
|
|
864
|
+
created = await ctx.db.createTask({
|
|
865
|
+
user_id: task.user_id,
|
|
866
|
+
uuid,
|
|
867
|
+
encrypted_payload: encryptedPayload,
|
|
868
|
+
next_send_at: nextSendAt,
|
|
869
|
+
message_type: messageType
|
|
870
|
+
});
|
|
871
|
+
} catch (error) {
|
|
872
|
+
if (isUniqueViolation(error)) return { created: false, reason: "duplicate", uuid };
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
if (!created) {
|
|
876
|
+
throw new Error("AGENTIC_SCHEDULE_FAILED: createTask \u6CA1\u6709\u8FD4\u56DE\u65B0\u5EFA\u7684\u4EFB\u52A1\u884C");
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
created: true,
|
|
880
|
+
id: created.id ?? null,
|
|
881
|
+
uuid: created.uuid ?? uuid,
|
|
882
|
+
nextSendAt: created.next_send_at ?? nextSendAt
|
|
883
|
+
};
|
|
884
|
+
};
|
|
760
885
|
const scratch = {};
|
|
761
886
|
const fireCtx = Object.freeze({
|
|
762
887
|
task: buildHookTask(task, decryptedPayload),
|
|
763
888
|
userId: task.user_id,
|
|
764
889
|
readState,
|
|
765
890
|
writeState,
|
|
891
|
+
scheduleTask,
|
|
766
892
|
now: new Date(nowFn()),
|
|
767
893
|
scratch
|
|
768
894
|
});
|
|
@@ -789,7 +915,11 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
789
915
|
}
|
|
790
916
|
const roundTimeoutMs = Math.max(1, Math.min(3e5, deadline - nowFn()));
|
|
791
917
|
const { response: llmResponse } = await callLlm(
|
|
792
|
-
{
|
|
918
|
+
{
|
|
919
|
+
...decryptedPayload,
|
|
920
|
+
messages,
|
|
921
|
+
...normalized.tools ? { tools: normalized.tools, toolChoice: normalized.toolChoice } : {}
|
|
922
|
+
},
|
|
793
923
|
{ requireContent: false, timeoutMs: roundTimeoutMs }
|
|
794
924
|
);
|
|
795
925
|
const assistantMessage = extractAssistantMessage(llmResponse);
|
|
@@ -807,7 +937,8 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
807
937
|
scratch
|
|
808
938
|
}),
|
|
809
939
|
readState,
|
|
810
|
-
writeState
|
|
940
|
+
writeState,
|
|
941
|
+
scheduleTask
|
|
811
942
|
});
|
|
812
943
|
const decision = await hooks.onLLMOutput(sessionCtx);
|
|
813
944
|
assertValidDecision(decision, { inlineToolCalls: true });
|
|
@@ -845,7 +976,10 @@ async function runAgenticFire({ task, decryptedPayload, userKey, ctx }) {
|
|
|
845
976
|
content: `Tool execution failed: ${error?.message ?? String(error)}`
|
|
846
977
|
}));
|
|
847
978
|
}
|
|
848
|
-
const
|
|
979
|
+
const nativeCalls = Array.isArray(assistantMessage.tool_calls) ? assistantMessage.tool_calls : [];
|
|
980
|
+
const nativeIds = new Set(nativeCalls.map((tc) => tc && tc.id));
|
|
981
|
+
const synthesized = toolCalls.filter((tc) => !nativeIds.has(tc && tc.id));
|
|
982
|
+
const assistantWithTools = synthesized.length === 0 ? assistantMessage : { ...assistantMessage, tool_calls: [...nativeCalls, ...synthesized] };
|
|
849
983
|
messages = [...messages.slice(0, -1), assistantWithTools, ...toolResults];
|
|
850
984
|
}
|
|
851
985
|
throw new Error(`AGENTIC_LOOP_EXCEEDED: no finish/skip-push decision within ${maxToolIterations} LLM round(s)`);
|
|
@@ -2262,7 +2396,7 @@ function createClientStateHandler(ctx) {
|
|
|
2262
2396
|
}
|
|
2263
2397
|
|
|
2264
2398
|
// src/server/lib/version.js
|
|
2265
|
-
var SERVER_VERSION = true ? "2.6.0-next.
|
|
2399
|
+
var SERVER_VERSION = true ? "2.6.0-next.9" : "0.0.0-dev";
|
|
2266
2400
|
|
|
2267
2401
|
// src/server/handlers/capabilities.js
|
|
2268
2402
|
var SERVER_FEATURES = Object.freeze([
|
|
@@ -2272,6 +2406,8 @@ var SERVER_FEATURES = Object.freeze([
|
|
|
2272
2406
|
"agentic-hooks",
|
|
2273
2407
|
"agentic-scratch",
|
|
2274
2408
|
"agentic-write-state",
|
|
2409
|
+
"agentic-fire-tools",
|
|
2410
|
+
"agentic-schedule-task",
|
|
2275
2411
|
"vapid-public-key"
|
|
2276
2412
|
]);
|
|
2277
2413
|
function createCapabilitiesHandler(ctx) {
|
|
@@ -2313,7 +2449,9 @@ function createSingleUserServer(config) {
|
|
|
2313
2449
|
hooks: config.hooks || null,
|
|
2314
2450
|
maxToolIterations: config.maxToolIterations,
|
|
2315
2451
|
totalTimeoutMs: config.totalTimeoutMs,
|
|
2316
|
-
maxStateValueBytes: config.maxStateValueBytes
|
|
2452
|
+
maxStateValueBytes: config.maxStateValueBytes,
|
|
2453
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
2454
|
+
maxScheduledTasksPerFire: config.maxScheduledTasksPerFire
|
|
2317
2455
|
};
|
|
2318
2456
|
return {
|
|
2319
2457
|
ctx,
|
|
@@ -2652,6 +2790,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
|
|
|
2652
2790
|
totalTimeoutMs: cfg.totalTimeoutMs,
|
|
2653
2791
|
// hook 的 ctx.writeState() 用它判断单条 value 的上限,和 PUT /client-state 同一个配置。
|
|
2654
2792
|
maxStateValueBytes: cfg.maxStateValueBytes,
|
|
2793
|
+
// hook 的 ctx.scheduleTask() 单次 fire 建任务的条数上限(默认 2)。
|
|
2794
|
+
maxScheduledTasksPerFire: cfg.maxScheduledTasksPerFire,
|
|
2655
2795
|
// 任务占位租期(默认 10 分钟,随 totalTimeoutMs 抬高)。
|
|
2656
2796
|
claimLeaseMs: cfg.claimLeaseMs
|
|
2657
2797
|
});
|