@musnows/scriverse 1.0.12 → 1.1.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/README.en.md +3 -0
- package/README.md +3 -0
- package/dist/ai-stream-steer.js +96 -0
- package/dist/ai-stream-steer.js.map +1 -0
- package/dist/ai-write-plans.js +44 -38
- package/dist/ai-write-plans.js.map +1 -1
- package/dist/ai.js +241 -128
- package/dist/ai.js.map +1 -1
- package/dist/app.js +29 -5
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +3 -0
- package/dist/attachment-storage.js.map +1 -1
- package/dist/chapter-annotation-note.js +14 -0
- package/dist/chapter-annotation-note.js.map +1 -0
- package/dist/character-structured-fields.js +7 -0
- package/dist/character-structured-fields.js.map +1 -0
- package/dist/cli-contract.js +1 -1
- package/dist/cli-contract.js.map +1 -1
- package/dist/im.js +2 -1
- package/dist/im.js.map +1 -1
- package/dist/public/ai-prompt-keyboard.js +5 -1
- package/dist/public/ai-prompt-queue.js +226 -0
- package/dist/public/ai-send-mode.js +31 -0
- package/dist/public/app.js +564 -37
- package/dist/public/character-profile.d.ts +7 -0
- package/dist/public/character-profile.js +123 -5
- package/dist/public/index.html +11 -4
- package/dist/public/styles.css +28 -1
- package/dist/server-runtime.js +2 -0
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +7 -6
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/ai.js
CHANGED
|
@@ -10,7 +10,9 @@ import { DEFAULT_AI_STREAM_IDLE_TIMEOUT_MS, normalizeAiStreamIdleTimeoutSeconds
|
|
|
10
10
|
import { DEFAULT_AI_CHAT_IMAGE_MAX_BYTES, formatUploadLimit } from "./upload-limits.js";
|
|
11
11
|
import { characterExtractionHash, characterExtractionSelectionFingerprint, editableCharacterExtractionCandidate, normalizeCharacterExtractionCandidate, parseStoredCharacterExtractionCandidates } from "./character-extraction.js";
|
|
12
12
|
import { AI_WRITE_TOOL_IDS, aiWritePlanOperationToolSchemas, askAiUserQuestionInputSchema } from "./ai-write-plans.js";
|
|
13
|
+
import { DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH } from "./chapter-annotation-note.js";
|
|
13
14
|
import { PLATFORM_AI_WORK_ID } from "./database.js";
|
|
15
|
+
import { AiStreamSteerMailbox, formatAiSteerInstruction, isAiSteerRefreshError, createAiSteerRefreshError } from "./ai-stream-steer.js";
|
|
14
16
|
import { AppError, notFound } from "./errors.js";
|
|
15
17
|
import { assertOfficialGoogleVertexBaseUrl, fetchGoogleOAuthAccessToken, GoogleVertexTokenCache, maskServiceAccountHint, parseGoogleServiceAccount } from "./google-vertex-auth.js";
|
|
16
18
|
import { HYBRID_SEARCH_TYPES, MAXIMUM_WORK_SEARCH_QUERY_LENGTH, buildHybridSearchSnippet, documentParagraphLineRangesFromLines, fuseHybridSearchChannels, hybridSearchPermissionModule, normalizeWorkSearchQuery } from "./hybrid-search.js";
|
|
@@ -1174,7 +1176,7 @@ const AGENT_TOOL_DEFINITIONS = {
|
|
|
1174
1176
|
}
|
|
1175
1177
|
}
|
|
1176
1178
|
};
|
|
1177
|
-
export function writePlanToolDefinition(toggles) {
|
|
1179
|
+
export function writePlanToolDefinition(toggles, chapterAnnotationNoteMaxLength = DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH) {
|
|
1178
1180
|
const entityTypes = [
|
|
1179
1181
|
...(toggles.settings ? ["setting"] : []),
|
|
1180
1182
|
...(toggles.characters ? ["character"] : []),
|
|
@@ -1184,7 +1186,7 @@ export function writePlanToolDefinition(toggles) {
|
|
|
1184
1186
|
...(toggles.relationships ? ["relationship"] : []),
|
|
1185
1187
|
...(toggles.outlines ? ["chapter-outline", "foreshadow"] : [])
|
|
1186
1188
|
];
|
|
1187
|
-
const operationSchemas = aiWritePlanOperationToolSchemas(toggles);
|
|
1189
|
+
const operationSchemas = aiWritePlanOperationToolSchemas(toggles, chapterAnnotationNoteMaxLength);
|
|
1188
1190
|
const operationTypes = [
|
|
1189
1191
|
...(entityTypes.length > 0 ? ["create_entry", "update_entry"] : []),
|
|
1190
1192
|
...(toggles.annotations ? ["create_annotation"] : []),
|
|
@@ -2033,7 +2035,16 @@ export class ContextBuilder {
|
|
|
2033
2035
|
const state = item.currentState;
|
|
2034
2036
|
const values = locked.map((key) => {
|
|
2035
2037
|
const entityValue = item[key];
|
|
2036
|
-
const
|
|
2038
|
+
const detailMatch = Array.isArray(attributes.details)
|
|
2039
|
+
? attributes.details.find((detail) => detail && typeof detail === "object" && !Array.isArray(detail)
|
|
2040
|
+
&& String(detail.label ?? "") === key)
|
|
2041
|
+
: undefined;
|
|
2042
|
+
const detailValue = detailMatch && typeof detailMatch === "object"
|
|
2043
|
+
? detailMatch.value
|
|
2044
|
+
: undefined;
|
|
2045
|
+
const value = entityValue === undefined || entityValue === null || entityValue === ""
|
|
2046
|
+
? attributes[key] ?? detailValue ?? state[key]
|
|
2047
|
+
: entityValue;
|
|
2037
2048
|
return `${key}=${String(value ?? "未填写")}`;
|
|
2038
2049
|
}).join(";");
|
|
2039
2050
|
return `- ${String(item.name)}:${values}`;
|
|
@@ -2453,10 +2464,18 @@ export class AiManager {
|
|
|
2453
2464
|
remoteMcp;
|
|
2454
2465
|
// 可写工具与用户提问的审批引擎:由应用装配层注入(app.ts),默认未注入 = 功能整体不可用。
|
|
2455
2466
|
aiWritePlanManager = null;
|
|
2467
|
+
steerMailbox = new AiStreamSteerMailbox();
|
|
2456
2468
|
/** 注入 AI 写入审批管理器;注入后 propose_write_plan / ask_user_question 才可能被启用。 */
|
|
2457
2469
|
attachWritePlanManager(manager) {
|
|
2458
2470
|
this.aiWritePlanManager = manager;
|
|
2459
2471
|
}
|
|
2472
|
+
enqueueSteer(workId, conversationId, content) {
|
|
2473
|
+
const conversation = this.store.getAiConversationSummary(conversationId);
|
|
2474
|
+
if (String(conversation.workId) !== workId) {
|
|
2475
|
+
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
2476
|
+
}
|
|
2477
|
+
return this.steerMailbox.enqueue(conversationId, content);
|
|
2478
|
+
}
|
|
2460
2479
|
constructor(store, vault, fetchImpl = fetch, validateOutboundUrl, authorizeTaskRun, attachmentStorage, options = {}) {
|
|
2461
2480
|
this.store = store;
|
|
2462
2481
|
this.vault = vault;
|
|
@@ -5133,21 +5152,35 @@ export class AiManager {
|
|
|
5133
5152
|
const processStartedAt = process.hrtime.bigint();
|
|
5134
5153
|
let persistedConversationMessage = null;
|
|
5135
5154
|
let streamedConversationContent = "";
|
|
5155
|
+
let assistantPersistRequestId = input.assistantMessageRequestId;
|
|
5136
5156
|
const persistStreamDelta = (delta) => {
|
|
5137
|
-
if (input.conversationId &&
|
|
5157
|
+
if (input.conversationId && assistantPersistRequestId && delta.length > 0) {
|
|
5138
5158
|
streamedConversationContent += delta;
|
|
5139
|
-
persistedConversationMessage = this.store.upsertAiConversationAssistantMessage(input.conversationId,
|
|
5159
|
+
persistedConversationMessage = this.store.upsertAiConversationAssistantMessage(input.conversationId, assistantPersistRequestId, streamedConversationContent);
|
|
5140
5160
|
}
|
|
5141
5161
|
onDelta(delta);
|
|
5142
5162
|
};
|
|
5143
5163
|
let generated;
|
|
5144
5164
|
try {
|
|
5145
|
-
generated = await this.generate({
|
|
5165
|
+
generated = await this.generate({
|
|
5166
|
+
...effectiveInput,
|
|
5167
|
+
taskType: "chat",
|
|
5168
|
+
onSteer: (event) => {
|
|
5169
|
+
if (input.conversationId && assistantPersistRequestId && streamedConversationContent.length > 0) {
|
|
5170
|
+
persistedConversationMessage = this.store.upsertAiConversationAssistantMessage(input.conversationId, assistantPersistRequestId, streamedConversationContent, {}, true);
|
|
5171
|
+
}
|
|
5172
|
+
assistantPersistRequestId = input.excludeConversationMessageId
|
|
5173
|
+
? `assistant:${input.excludeConversationMessageId}:after:${event.id}`
|
|
5174
|
+
: `assistant:steer:${event.id}`;
|
|
5175
|
+
streamedConversationContent = "";
|
|
5176
|
+
input.onSteer?.(event);
|
|
5177
|
+
}
|
|
5178
|
+
}, persistStreamDelta);
|
|
5146
5179
|
}
|
|
5147
5180
|
catch (error) {
|
|
5148
|
-
if (persistedConversationMessage && input.conversationId &&
|
|
5181
|
+
if (persistedConversationMessage && input.conversationId && assistantPersistRequestId) {
|
|
5149
5182
|
const interruptionCode = error instanceof AppError ? error.code : "AI_STREAM_FAILED";
|
|
5150
|
-
persistedConversationMessage = this.store.upsertAiConversationAssistantMessage(input.conversationId,
|
|
5183
|
+
persistedConversationMessage = this.store.upsertAiConversationAssistantMessage(input.conversationId, assistantPersistRequestId, streamedConversationContent, { interrupted: true, interruptionCode }, true);
|
|
5151
5184
|
}
|
|
5152
5185
|
throw error;
|
|
5153
5186
|
}
|
|
@@ -5179,8 +5212,8 @@ export class AiManager {
|
|
|
5179
5212
|
: "已向你提出问题,等待回答后继续。";
|
|
5180
5213
|
if (!streamedConversationContent.trim())
|
|
5181
5214
|
persistStreamDelta(suspendedContent);
|
|
5182
|
-
const conversationMessage = input.conversationId &&
|
|
5183
|
-
? this.store.upsertAiConversationAssistantMessage(input.conversationId,
|
|
5215
|
+
const conversationMessage = input.conversationId && assistantPersistRequestId
|
|
5216
|
+
? this.store.upsertAiConversationAssistantMessage(input.conversationId, assistantPersistRequestId, suspendedContent, generatedMessageMetadata, true)
|
|
5184
5217
|
: persistedConversationMessage;
|
|
5185
5218
|
return {
|
|
5186
5219
|
id: `question:${generated.suspendedQuestionId}`,
|
|
@@ -5197,8 +5230,10 @@ export class AiManager {
|
|
|
5197
5230
|
...(conversationMessage ? { conversationMessage } : {})
|
|
5198
5231
|
};
|
|
5199
5232
|
}
|
|
5200
|
-
const conversationMessage = input.conversationId &&
|
|
5201
|
-
? this.store.upsertAiConversationAssistantMessage(input.conversationId, input.assistantMessageRequestId
|
|
5233
|
+
const conversationMessage = input.conversationId && assistantPersistRequestId
|
|
5234
|
+
? this.store.upsertAiConversationAssistantMessage(input.conversationId, assistantPersistRequestId, assistantPersistRequestId === input.assistantMessageRequestId
|
|
5235
|
+
? generated.content
|
|
5236
|
+
: streamedConversationContent, {
|
|
5202
5237
|
...generatedMessageMetadata,
|
|
5203
5238
|
...(activeWritingSkillName && !generated.toolCallLimit ? { activeSkills: [activeWritingSkillName] } : {}),
|
|
5204
5239
|
...(input.toolContinuation
|
|
@@ -6897,11 +6932,13 @@ export class AiManager {
|
|
|
6897
6932
|
}
|
|
6898
6933
|
enabledAgentTools(workId, taskType, requestedToolIds, conversationId, roleplayCharacterIdOverride) {
|
|
6899
6934
|
const toolIds = this.enabledAgentToolIds(workId, taskType, requestedToolIds, conversationId, roleplayCharacterIdOverride);
|
|
6900
|
-
const
|
|
6901
|
-
|
|
6935
|
+
const writePlanManager = this.aiWritePlanManager;
|
|
6936
|
+
const writeToggles = writePlanManager && conversationId
|
|
6937
|
+
? writePlanManager.getConversationTools(workId, conversationId)
|
|
6902
6938
|
: null;
|
|
6939
|
+
const chapterAnnotationNoteMaxLength = writePlanManager?.chapterAnnotationNoteMaxLength ?? DEFAULT_CHAPTER_ANNOTATION_NOTE_MAX_LENGTH;
|
|
6903
6940
|
const builtInTools = toolIds.map((toolId) => toolId === "propose_write_plan" && writeToggles
|
|
6904
|
-
? writePlanToolDefinition(writeToggles)
|
|
6941
|
+
? writePlanToolDefinition(writeToggles, chapterAnnotationNoteMaxLength)
|
|
6905
6942
|
: AGENT_TOOL_DEFINITIONS[toolId]);
|
|
6906
6943
|
const roleplayCharacterId = roleplayCharacterIdOverride === undefined
|
|
6907
6944
|
? this.roleplayCharacterId(workId, conversationId)
|
|
@@ -8929,11 +8966,24 @@ export class AiManager {
|
|
|
8929
8966
|
}
|
|
8930
8967
|
}
|
|
8931
8968
|
const controller = new AbortController();
|
|
8969
|
+
const steerRefreshController = new AbortController();
|
|
8970
|
+
const detachSteerRefresh = input.conversationId && input.taskType === "chat"
|
|
8971
|
+
? this.steerMailbox.subscribe(input.conversationId, steerRefreshController)
|
|
8972
|
+
: () => undefined;
|
|
8932
8973
|
const forwardAbort = () => controller.abort(input.signal?.reason);
|
|
8974
|
+
const forwardSteerRefresh = () => {
|
|
8975
|
+
if (!controller.signal.aborted) {
|
|
8976
|
+
controller.abort(steerRefreshController.signal.reason ?? createAiSteerRefreshError());
|
|
8977
|
+
}
|
|
8978
|
+
};
|
|
8933
8979
|
if (input.signal?.aborted)
|
|
8934
8980
|
forwardAbort();
|
|
8935
8981
|
else
|
|
8936
8982
|
input.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
8983
|
+
if (steerRefreshController.signal.aborted)
|
|
8984
|
+
forwardSteerRefresh();
|
|
8985
|
+
else
|
|
8986
|
+
steerRefreshController.signal.addEventListener("abort", forwardSteerRefresh, { once: true });
|
|
8937
8987
|
const streamWatchdog = streamResponse
|
|
8938
8988
|
? new InteractiveStreamIdleWatchdog(controller, this.interactiveStreamIdleTimeoutMs)
|
|
8939
8989
|
: null;
|
|
@@ -9004,6 +9054,9 @@ export class AiManager {
|
|
|
9004
9054
|
}
|
|
9005
9055
|
throw interactiveStreamRequestCancelledError();
|
|
9006
9056
|
}
|
|
9057
|
+
if (streamResponse && !input.signal?.aborted && isAiSteerRefreshError(controller.signal.reason)) {
|
|
9058
|
+
throw createAiSteerRefreshError();
|
|
9059
|
+
}
|
|
9007
9060
|
if (streamWatchdog?.failure)
|
|
9008
9061
|
throw streamWatchdog.failure;
|
|
9009
9062
|
if (streamResponse && !responseReceived) {
|
|
@@ -9016,6 +9069,7 @@ export class AiManager {
|
|
|
9016
9069
|
clearTimeout(timeout);
|
|
9017
9070
|
streamWatchdog?.dispose();
|
|
9018
9071
|
input.signal?.removeEventListener("abort", forwardAbort);
|
|
9072
|
+
detachSteerRefresh();
|
|
9019
9073
|
}
|
|
9020
9074
|
}, input.beforeRequest);
|
|
9021
9075
|
logger.info("ai.call.attempt_completed", {
|
|
@@ -9083,6 +9137,22 @@ export class AiManager {
|
|
|
9083
9137
|
}
|
|
9084
9138
|
}
|
|
9085
9139
|
catch (error) {
|
|
9140
|
+
if (isAiSteerRefreshError(error)) {
|
|
9141
|
+
const parsed = {
|
|
9142
|
+
choices: [{
|
|
9143
|
+
finish_reason: "stop",
|
|
9144
|
+
message: { content: streamedRoundContent || null }
|
|
9145
|
+
}]
|
|
9146
|
+
};
|
|
9147
|
+
completionDelivery.set(parsed, streamResponse ? "sse" : "json");
|
|
9148
|
+
if (streamResponse && purpose === "generation" && streamedRoundContent.length > 0) {
|
|
9149
|
+
streamedContent += streamedRoundContent;
|
|
9150
|
+
}
|
|
9151
|
+
traceAttempt.completedAt = now();
|
|
9152
|
+
traceAttempt.status = "completed";
|
|
9153
|
+
saveTrace();
|
|
9154
|
+
return parsed;
|
|
9155
|
+
}
|
|
9086
9156
|
lastFailure = error;
|
|
9087
9157
|
if (requestAttemptLimit !== null && !failureCounted)
|
|
9088
9158
|
requestFailureCount += 1;
|
|
@@ -9272,128 +9342,171 @@ export class AiManager {
|
|
|
9272
9342
|
let suspendedQuestionId = null;
|
|
9273
9343
|
let toolCallLimit;
|
|
9274
9344
|
let toolCallLimitMessage = "";
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
const
|
|
9279
|
-
if (
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9345
|
+
const commitAssistantChoice = () => {
|
|
9346
|
+
if (!choice?.message || choice.message.tool_calls?.length)
|
|
9347
|
+
return;
|
|
9348
|
+
const content = typeof choice.message.content === "string" ? choice.message.content : "";
|
|
9349
|
+
if (!content.trim() && !choice.message.reasoning_content)
|
|
9350
|
+
return;
|
|
9351
|
+
completionMessages.push({
|
|
9352
|
+
role: "assistant",
|
|
9353
|
+
content: content || null,
|
|
9354
|
+
reasoning_content: choice.message.reasoning_content ?? null
|
|
9355
|
+
});
|
|
9356
|
+
};
|
|
9357
|
+
const applyPendingSteers = () => {
|
|
9358
|
+
if (!input.conversationId || input.taskType !== "chat")
|
|
9359
|
+
return false;
|
|
9360
|
+
const pending = this.steerMailbox.drain(input.conversationId);
|
|
9361
|
+
if (!pending.length)
|
|
9362
|
+
return false;
|
|
9363
|
+
commitAssistantChoice();
|
|
9364
|
+
for (const steer of pending) {
|
|
9365
|
+
const message = this.store.addAiConversationMessage(input.conversationId, {
|
|
9366
|
+
role: "user",
|
|
9367
|
+
content: steer.content,
|
|
9368
|
+
requestId: `steer:${steer.id}`,
|
|
9369
|
+
metadata: { kind: "steer" }
|
|
9290
9370
|
});
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9371
|
+
completionMessages.push({
|
|
9372
|
+
role: "user",
|
|
9373
|
+
content: formatAiSteerInstruction(steer.content)
|
|
9374
|
+
});
|
|
9375
|
+
input.onSteer?.({ id: steer.id, content: steer.content, message });
|
|
9295
9376
|
}
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9377
|
+
return true;
|
|
9378
|
+
};
|
|
9379
|
+
generationTurn: for (;;) {
|
|
9380
|
+
while (choice?.message?.tool_calls?.length) {
|
|
9381
|
+
const round = toolRound + 1;
|
|
9382
|
+
recordChoiceProcess(payload, round, true);
|
|
9383
|
+
const toolCalls = choice.message.tool_calls;
|
|
9384
|
+
if (shouldRejectGlobalToolCalls(globalToolCallUsed, toolCalls.length, globalToolCallLimit)) {
|
|
9385
|
+
logger.warn("ai.tool_call.global_limit_reached", {
|
|
9386
|
+
callId,
|
|
9387
|
+
workId: input.workId,
|
|
9388
|
+
agentToolCallLimit,
|
|
9389
|
+
globalLimit: globalToolCallLimit,
|
|
9390
|
+
actualCalls: globalToolCallUsed,
|
|
9391
|
+
requestedCalls: toolCalls.length,
|
|
9392
|
+
compactCount: toolContextCompactCount,
|
|
9393
|
+
turnQuotaUsed: toolCallQuotaUsed,
|
|
9394
|
+
toolsCalled: executedToolCalls.map((item) => item.name)
|
|
9395
|
+
});
|
|
9396
|
+
toolCallLimit = { scope: "global", used: globalToolCallUsed, limit: globalToolCallLimit, requested: toolCalls.length };
|
|
9301
9397
|
}
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
const normalizedToolCalls = toolCalls.map((toolCall) => ({
|
|
9305
|
-
...toolCall,
|
|
9306
|
-
function: {
|
|
9307
|
-
...toolCall.function,
|
|
9308
|
-
arguments: typeof toolCall.function.arguments === "string" ? toolCall.function.arguments : JSON.stringify(toolCall.function.arguments ?? {})
|
|
9398
|
+
if (!toolCallLimit && shouldRejectAgentToolCalls(toolCallQuotaUsed, toolCalls.length, agentToolCallLimit)) {
|
|
9399
|
+
toolCallLimit = { scope: "cycle", used: toolCallQuotaUsed, limit: agentToolCallLimit, requested: toolCalls.length };
|
|
9309
9400
|
}
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
}
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
executedToolCalls.push(toolExecution);
|
|
9343
|
-
toolCallQuotaUsed += 1;
|
|
9344
|
-
globalToolCallUsed += 1;
|
|
9345
|
-
const remainingToolCalls = Math.max(0, agentToolCallLimit - toolCallQuotaUsed);
|
|
9346
|
-
toolExecution.result = withAgentToolCallQuotaNotice(toolExecution.result, remainingToolCalls, agentToolCallLimit);
|
|
9347
|
-
toolTraceRound?.toolExecutions.push(toolExecution);
|
|
9348
|
-
saveTrace();
|
|
9349
|
-
processSteps.push({ id: id("process"), type: "tool", round, toolCall: toolExecution, createdAt: toolExecution.calledAt });
|
|
9350
|
-
input.onToolCall?.(toolExecution, round, permissionModules);
|
|
9351
|
-
currentRoundMessages.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(toolExecution.result) });
|
|
9352
|
-
const questionId = toolExecution.name === "ask_user_question" && toolExecution.status === "completed"
|
|
9353
|
-
? String(toolExecution.result.question?.id ?? "")
|
|
9354
|
-
: "";
|
|
9355
|
-
if (questionId) {
|
|
9356
|
-
this.aiWritePlanManager?.saveQuestionContinuation(questionId, {
|
|
9357
|
-
workId: input.workId,
|
|
9401
|
+
if (toolCallLimit) {
|
|
9402
|
+
const limitLabel = toolCallLimit.scope === "global" ? "整次回答的全局上限" : "本轮调用上限";
|
|
9403
|
+
toolCallLimitMessage = `Agent 工具调用额度不足:${limitLabel} ${toolCallLimit.limit} 次,已用 ${toolCallLimit.used} 次,模型还请求 ${toolCallLimit.requested} 次。\n\n已保留此前的回复与执行过程。你可以在本书 AI 设置中调整“Agent 工具调用上限”${toolCallLimit.scope === "global" ? "或全局倍数" : ""},再发送“继续”完成剩余工作。`;
|
|
9404
|
+
if (input.taskType !== "chat" || input.im) {
|
|
9405
|
+
throw new AppError(409, "AI_TOOL_CALL_LIMIT_REACHED", toolCallLimitMessage, { toolCallLimit });
|
|
9406
|
+
}
|
|
9407
|
+
break;
|
|
9408
|
+
}
|
|
9409
|
+
const normalizedToolCalls = toolCalls.map((toolCall) => ({
|
|
9410
|
+
...toolCall,
|
|
9411
|
+
function: {
|
|
9412
|
+
...toolCall.function,
|
|
9413
|
+
arguments: typeof toolCall.function.arguments === "string" ? toolCall.function.arguments : JSON.stringify(toolCall.function.arguments ?? {})
|
|
9414
|
+
}
|
|
9415
|
+
}));
|
|
9416
|
+
const toolTraceRound = traceRounds.at(-1);
|
|
9417
|
+
const assistantToolMessage = {
|
|
9418
|
+
role: "assistant",
|
|
9419
|
+
content: choice.message.content ?? null,
|
|
9420
|
+
reasoning_content: choice.message.reasoning_content ?? null,
|
|
9421
|
+
tool_calls: normalizedToolCalls,
|
|
9422
|
+
...(choice.message.anthropic_content?.length ? { anthropic_content: choice.message.anthropic_content } : {})
|
|
9423
|
+
};
|
|
9424
|
+
if (shouldCompactBeforeToolRound(assistantToolMessage, toolCalls.length)) {
|
|
9425
|
+
await compactToolContext([], round);
|
|
9426
|
+
}
|
|
9427
|
+
const maximumResultChars = toolResultMaximumChars(assistantToolMessage, toolCalls.length);
|
|
9428
|
+
const currentRoundMessages = [assistantToolMessage];
|
|
9429
|
+
const nativeImageMessages = [];
|
|
9430
|
+
for (const toolCall of toolCalls) {
|
|
9431
|
+
input.beforeRequest?.();
|
|
9432
|
+
const execution = await this.executeAgentTool(input.workId, toolCall, maximumResultChars, generationRoleplayCharacterId, allowedToolIds, input.signal, trackUsage, input.scope, model, provider, {
|
|
9358
9433
|
conversationId: input.conversationId ?? null,
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9434
|
+
im: Boolean(input.im),
|
|
9435
|
+
listImMembers: input.im?.listMembers,
|
|
9436
|
+
searchImMessages: input.im?.searchMessages
|
|
9437
|
+
}, stagedRoleplayMemoryCandidates, allowedRemoteMcpToolNames, input.beforeRequest);
|
|
9438
|
+
const { nativeImage, ...toolExecution } = execution;
|
|
9439
|
+
const permissionModules = this.executedAgentToolPermissionModules(input.workId, toolExecution);
|
|
9440
|
+
logger.info("ai.tool_call.completed", {
|
|
9441
|
+
callId,
|
|
9442
|
+
toolName: toolExecution.name,
|
|
9443
|
+
status: toolExecution.status,
|
|
9368
9444
|
round,
|
|
9369
|
-
|
|
9445
|
+
maximumResultChars
|
|
9370
9446
|
});
|
|
9371
|
-
|
|
9372
|
-
|
|
9447
|
+
executedToolCalls.push(toolExecution);
|
|
9448
|
+
toolCallQuotaUsed += 1;
|
|
9449
|
+
globalToolCallUsed += 1;
|
|
9450
|
+
const remainingToolCalls = Math.max(0, agentToolCallLimit - toolCallQuotaUsed);
|
|
9451
|
+
toolExecution.result = withAgentToolCallQuotaNotice(toolExecution.result, remainingToolCalls, agentToolCallLimit);
|
|
9452
|
+
toolTraceRound?.toolExecutions.push(toolExecution);
|
|
9453
|
+
saveTrace();
|
|
9454
|
+
processSteps.push({ id: id("process"), type: "tool", round, toolCall: toolExecution, createdAt: toolExecution.calledAt });
|
|
9455
|
+
input.onToolCall?.(toolExecution, round, permissionModules);
|
|
9456
|
+
currentRoundMessages.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(toolExecution.result) });
|
|
9457
|
+
const questionId = toolExecution.name === "ask_user_question" && toolExecution.status === "completed"
|
|
9458
|
+
? String(toolExecution.result.question?.id ?? "")
|
|
9459
|
+
: "";
|
|
9460
|
+
if (questionId) {
|
|
9461
|
+
this.aiWritePlanManager?.saveQuestionContinuation(questionId, {
|
|
9462
|
+
workId: input.workId,
|
|
9463
|
+
conversationId: input.conversationId ?? null,
|
|
9464
|
+
scope: input.scope,
|
|
9465
|
+
modelId: input.modelId ?? stringValue(model, "id"),
|
|
9466
|
+
toolCallId: toolCall.id,
|
|
9467
|
+
assistantMessageRequestId: input.assistantMessageRequestId ?? null,
|
|
9468
|
+
toolMessages: sanitizeCompletionTraceMessages([
|
|
9469
|
+
...(input.toolContinuation ? resolvedQuestionToolMessages(input.toolContinuation) : []),
|
|
9470
|
+
...(compactedToolContextMessage ? [compactedToolContextMessage] : completionMessages.slice(baseMessageCount)),
|
|
9471
|
+
...currentRoundMessages
|
|
9472
|
+
]),
|
|
9473
|
+
round,
|
|
9474
|
+
createdAt: now()
|
|
9475
|
+
});
|
|
9476
|
+
suspendedQuestionId = questionId;
|
|
9477
|
+
break;
|
|
9478
|
+
}
|
|
9479
|
+
if (nativeImage) {
|
|
9480
|
+
nativeImageMessages.push({
|
|
9481
|
+
role: "user",
|
|
9482
|
+
content: [
|
|
9483
|
+
{ type: "text", text: "image 工具已将图片作为原生多模态内容附在本条消息中。请直接理解这张图片,不要再次调用 image 工具读取它。" },
|
|
9484
|
+
{ type: "image_url", image_url: { url: nativeImage.dataUrl, detail: "auto" } }
|
|
9485
|
+
]
|
|
9486
|
+
});
|
|
9487
|
+
}
|
|
9373
9488
|
}
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
{ type: "image_url", image_url: { url: nativeImage.dataUrl, detail: "auto" } }
|
|
9380
|
-
]
|
|
9381
|
-
});
|
|
9489
|
+
currentRoundMessages.push(...nativeImageMessages);
|
|
9490
|
+
const projectedMessages = [...completionMessages, ...currentRoundMessages];
|
|
9491
|
+
try {
|
|
9492
|
+
this.constrainParametersForContext(model, projectedMessages, parameters, tools);
|
|
9493
|
+
completionMessages.push(...currentRoundMessages);
|
|
9382
9494
|
}
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
}
|
|
9395
|
-
|
|
9396
|
-
|
|
9495
|
+
catch (error) {
|
|
9496
|
+
if (!(error instanceof AppError) || error.code !== "CONTEXT_WINDOW_EXCEEDED")
|
|
9497
|
+
throw error;
|
|
9498
|
+
await compactToolContext(currentRoundMessages, round);
|
|
9499
|
+
}
|
|
9500
|
+
toolRound += 1;
|
|
9501
|
+
if (suspendedQuestionId)
|
|
9502
|
+
break generationTurn;
|
|
9503
|
+
applyPendingSteers();
|
|
9504
|
+
payload = await requestCompletion("auto");
|
|
9505
|
+
choice = payload.choices?.[0];
|
|
9506
|
+
}
|
|
9507
|
+
if (suspendedQuestionId || toolCallLimit)
|
|
9508
|
+
break;
|
|
9509
|
+
if (!applyPendingSteers())
|
|
9397
9510
|
break;
|
|
9398
9511
|
payload = await requestCompletion("auto");
|
|
9399
9512
|
choice = payload.choices?.[0];
|