@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/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 value = entityValue === undefined || entityValue === null || entityValue === "" ? attributes[key] ?? state[key] : entityValue;
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 && input.assistantMessageRequestId && delta.length > 0) {
5157
+ if (input.conversationId && assistantPersistRequestId && delta.length > 0) {
5138
5158
  streamedConversationContent += delta;
5139
- persistedConversationMessage = this.store.upsertAiConversationAssistantMessage(input.conversationId, input.assistantMessageRequestId, streamedConversationContent);
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({ ...effectiveInput, taskType: "chat" }, persistStreamDelta);
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 && input.assistantMessageRequestId) {
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, input.assistantMessageRequestId, streamedConversationContent, { interrupted: true, interruptionCode }, true);
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 && input.assistantMessageRequestId
5183
- ? this.store.upsertAiConversationAssistantMessage(input.conversationId, input.assistantMessageRequestId, suspendedContent, generatedMessageMetadata, true)
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 && input.assistantMessageRequestId
5201
- ? this.store.upsertAiConversationAssistantMessage(input.conversationId, input.assistantMessageRequestId, generated.content, {
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 writeToggles = this.aiWritePlanManager && conversationId
6901
- ? this.aiWritePlanManager.getConversationTools(workId, conversationId)
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
- while (choice?.message?.tool_calls?.length) {
9276
- const round = toolRound + 1;
9277
- recordChoiceProcess(payload, round, true);
9278
- const toolCalls = choice.message.tool_calls;
9279
- if (shouldRejectGlobalToolCalls(globalToolCallUsed, toolCalls.length, globalToolCallLimit)) {
9280
- logger.warn("ai.tool_call.global_limit_reached", {
9281
- callId,
9282
- workId: input.workId,
9283
- agentToolCallLimit,
9284
- globalLimit: globalToolCallLimit,
9285
- actualCalls: globalToolCallUsed,
9286
- requestedCalls: toolCalls.length,
9287
- compactCount: toolContextCompactCount,
9288
- turnQuotaUsed: toolCallQuotaUsed,
9289
- toolsCalled: executedToolCalls.map((item) => item.name)
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
- toolCallLimit = { scope: "global", used: globalToolCallUsed, limit: globalToolCallLimit, requested: toolCalls.length };
9292
- }
9293
- if (!toolCallLimit && shouldRejectAgentToolCalls(toolCallQuotaUsed, toolCalls.length, agentToolCallLimit)) {
9294
- toolCallLimit = { scope: "cycle", used: toolCallQuotaUsed, limit: agentToolCallLimit, requested: toolCalls.length };
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
- if (toolCallLimit) {
9297
- const limitLabel = toolCallLimit.scope === "global" ? "整次回答的全局上限" : "本轮调用上限";
9298
- toolCallLimitMessage = `Agent 工具调用额度不足:${limitLabel} ${toolCallLimit.limit} 次,已用 ${toolCallLimit.used} 次,模型还请求 ${toolCallLimit.requested} 次。\n\n已保留此前的回复与执行过程。你可以在本书 AI 设置中调整“Agent 工具调用上限”${toolCallLimit.scope === "global" ? "或全局倍数" : ""},再发送“继续”完成剩余工作。`;
9299
- if (input.taskType !== "chat" || input.im) {
9300
- throw new AppError(409, "AI_TOOL_CALL_LIMIT_REACHED", toolCallLimitMessage, { toolCallLimit });
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
- break;
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
- const toolTraceRound = traceRounds.at(-1);
9312
- const assistantToolMessage = {
9313
- role: "assistant",
9314
- content: choice.message.content ?? null,
9315
- reasoning_content: choice.message.reasoning_content ?? null,
9316
- tool_calls: normalizedToolCalls,
9317
- ...(choice.message.anthropic_content?.length ? { anthropic_content: choice.message.anthropic_content } : {})
9318
- };
9319
- if (shouldCompactBeforeToolRound(assistantToolMessage, toolCalls.length)) {
9320
- await compactToolContext([], round);
9321
- }
9322
- const maximumResultChars = toolResultMaximumChars(assistantToolMessage, toolCalls.length);
9323
- const currentRoundMessages = [assistantToolMessage];
9324
- const nativeImageMessages = [];
9325
- for (const toolCall of toolCalls) {
9326
- input.beforeRequest?.();
9327
- const execution = await this.executeAgentTool(input.workId, toolCall, maximumResultChars, generationRoleplayCharacterId, allowedToolIds, input.signal, trackUsage, input.scope, model, provider, {
9328
- conversationId: input.conversationId ?? null,
9329
- im: Boolean(input.im),
9330
- listImMembers: input.im?.listMembers,
9331
- searchImMessages: input.im?.searchMessages
9332
- }, stagedRoleplayMemoryCandidates, allowedRemoteMcpToolNames, input.beforeRequest);
9333
- const { nativeImage, ...toolExecution } = execution;
9334
- const permissionModules = this.executedAgentToolPermissionModules(input.workId, toolExecution);
9335
- logger.info("ai.tool_call.completed", {
9336
- callId,
9337
- toolName: toolExecution.name,
9338
- status: toolExecution.status,
9339
- round,
9340
- maximumResultChars
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
- scope: input.scope,
9360
- modelId: input.modelId ?? stringValue(model, "id"),
9361
- toolCallId: toolCall.id,
9362
- assistantMessageRequestId: input.assistantMessageRequestId ?? null,
9363
- toolMessages: sanitizeCompletionTraceMessages([
9364
- ...(input.toolContinuation ? resolvedQuestionToolMessages(input.toolContinuation) : []),
9365
- ...(compactedToolContextMessage ? [compactedToolContextMessage] : completionMessages.slice(baseMessageCount)),
9366
- ...currentRoundMessages
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
- createdAt: now()
9445
+ maximumResultChars
9370
9446
  });
9371
- suspendedQuestionId = questionId;
9372
- break;
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
- if (nativeImage) {
9375
- nativeImageMessages.push({
9376
- role: "user",
9377
- content: [
9378
- { type: "text", text: "image 工具已将图片作为原生多模态内容附在本条消息中。请直接理解这张图片,不要再次调用 image 工具读取它。" },
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
- currentRoundMessages.push(...nativeImageMessages);
9385
- const projectedMessages = [...completionMessages, ...currentRoundMessages];
9386
- try {
9387
- this.constrainParametersForContext(model, projectedMessages, parameters, tools);
9388
- completionMessages.push(...currentRoundMessages);
9389
- }
9390
- catch (error) {
9391
- if (!(error instanceof AppError) || error.code !== "CONTEXT_WINDOW_EXCEEDED")
9392
- throw error;
9393
- await compactToolContext(currentRoundMessages, round);
9394
- }
9395
- toolRound += 1;
9396
- if (suspendedQuestionId)
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];