@musnows/scriverse 0.7.6 → 0.7.8

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
@@ -51,6 +51,8 @@ const AUTO_RUN_RETRY_DELAYS_MS = [5_000, 30_000];
51
51
  const AI_INTERACTIVE_TIMEOUT_MS = 60_000;
52
52
  const AI_LONG_RUNNING_TIMEOUT_MS = 300_000;
53
53
  const FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT = 95;
54
+ const MIN_OUTPUT_RESERVE_TOKENS = 1_024;
55
+ const MIN_CONTEXT_REMAINING_TOKENS = 5_000;
54
56
  const analysisTaskTypes = new Set(ANALYSIS_TASK_TYPES);
55
57
  const interactiveStreamErrorCodes = new Set([
56
58
  "AI_STREAM_IDLE_TIMEOUT",
@@ -173,6 +175,7 @@ export function autoRunFailureDisposition(error, attemptCount) {
173
175
  }
174
176
  const allowedParameters = new Set(["temperature", "top_p", "max_tokens", "presence_penalty", "frequency_penalty", "seed"]);
175
177
  const DEFAULT_MAX_TOKENS = 32_000;
178
+ const MAX_MODEL_OUTPUT_TOKENS = 2_000_000;
176
179
  const DEFAULT_CONTEXT_WINDOW = 128_000;
177
180
  const RELATIONSHIP_MAX_FUZZY_REFERENCES = 32;
178
181
  const RELATIONSHIP_MAX_FUZZY_SOURCES = 200;
@@ -405,7 +408,7 @@ function sanitizeCompletionTraceResponse(value) {
405
408
  }
406
409
  const MAX_AGENT_TOOL_CALLS = 12;
407
410
  const TOOL_CONTEXT_COMPACT_MAX_TOKENS = 1_024;
408
- const TOOL_CONTEXT_RESPONSE_RESERVE_TOKENS = 512;
411
+ const TOOL_CONTEXT_RESPONSE_RESERVE_TOKENS = MIN_OUTPUT_RESERVE_TOKENS;
409
412
  const IMAGE_TOOL_MAX_BYTES = 30 * 1024 * 1024;
410
413
  const IMAGE_TOOL_MAX_OUTPUT_TOKENS = 8_192;
411
414
  const agentToolCursor = z.number().int().min(0).max(100_000).default(0);
@@ -738,7 +741,7 @@ function completionPayloadOutputText(payload) {
738
741
  }
739
742
  function normalizeModelPreset(input, modelId = "") {
740
743
  const maxTokens = typeof input.max_tokens === "number" && Number.isFinite(input.max_tokens)
741
- ? Math.round(clamp(input.max_tokens, 1, 32_768))
744
+ ? Math.round(clamp(input.max_tokens, 1, MAX_MODEL_OUTPUT_TOKENS))
742
745
  : DEFAULT_MAX_TOKENS;
743
746
  const temperature = input.temperature;
744
747
  const defaultTemperature = isKimiModelId(modelId) && !(typeof temperature === "number" && Number.isFinite(temperature))
@@ -3081,7 +3084,9 @@ export class AiManager {
3081
3084
  const effectiveInput = input.taskType === "continue"
3082
3085
  ? { ...input, scope: this.enrichContinuationScope(input.workId, input.scope, input.instruction) }
3083
3086
  : input;
3087
+ const processStartedAt = process.hrtime.bigint();
3084
3088
  const generated = await this.generate(effectiveInput);
3089
+ const processDurationMs = Math.min(86_400_000, Math.max(0, Math.round(Number(process.hrtime.bigint() - processStartedAt) / 1_000_000)));
3085
3090
  const chapter = effectiveInput.scope.chapterId ? this.store.getChapter(effectiveInput.scope.chapterId) : null;
3086
3091
  const suggestionId = id("suggestion");
3087
3092
  this.store.db.run(`INSERT INTO ai_suggestions (id, call_id, work_id, chapter_id, chapter_version, task_type, instruction,
@@ -3091,6 +3096,7 @@ export class AiManager {
3091
3096
  return {
3092
3097
  ...this.getSuggestion(suggestionId),
3093
3098
  outputTokens: generated.outputTokens,
3099
+ processDurationMs,
3094
3100
  ...(generated.cacheHitPercent === undefined ? {} : { cacheHitPercent: generated.cacheHitPercent }),
3095
3101
  toolCalls: generated.toolCalls,
3096
3102
  processSteps: generated.processSteps,
@@ -3112,7 +3118,9 @@ export class AiManager {
3112
3118
  && firstUserContent
3113
3119
  && titleModelId
3114
3120
  && (conversationBefore?.title === "新对话" || conversationBefore?.title === defaultTitle));
3121
+ const processStartedAt = process.hrtime.bigint();
3115
3122
  const generated = await this.generate({ ...input, taskType: "chat" }, onDelta);
3123
+ const processDurationMs = Math.min(86_400_000, Math.max(0, Math.round(Number(process.hrtime.bigint() - processStartedAt) / 1_000_000)));
3116
3124
  const chapter = input.scope.chapterId ? this.store.getChapter(input.scope.chapterId) : null;
3117
3125
  const suggestionId = id("suggestion");
3118
3126
  this.store.db.run(`INSERT INTO ai_suggestions (id, call_id, work_id, chapter_id, chapter_version, task_type, instruction,
@@ -3126,6 +3134,7 @@ export class AiManager {
3126
3134
  metadata: {
3127
3135
  ...(modelDisplayName ? { modelDisplayName } : {}),
3128
3136
  outputTokens: generated.outputTokens,
3137
+ processDurationMs,
3129
3138
  ...(generated.reasoningContent === undefined ? {} : { reasoningContent: generated.reasoningContent }),
3130
3139
  ...(generated.cacheHitPercent === undefined ? {} : { cacheHitPercent: generated.cacheHitPercent }),
3131
3140
  toolCalls: generated.toolCalls,
@@ -3142,6 +3151,7 @@ export class AiManager {
3142
3151
  return {
3143
3152
  ...this.getSuggestion(suggestionId),
3144
3153
  outputTokens: generated.outputTokens,
3154
+ processDurationMs,
3145
3155
  ...(generated.cacheHitPercent === undefined ? {} : { cacheHitPercent: generated.cacheHitPercent }),
3146
3156
  toolCalls: generated.toolCalls,
3147
3157
  processSteps: generated.processSteps,
@@ -3571,7 +3581,7 @@ export class AiManager {
3571
3581
  const contextWindow = numberValue(model, "context_window") || DEFAULT_CONTEXT_WINDOW;
3572
3582
  const preset = safeJsonObject(stringValue(model, "preset_json"));
3573
3583
  const configuredOutputTokens = typeof preset.max_tokens === "number" ? preset.max_tokens : DEFAULT_MAX_TOKENS;
3574
- const outputReserveTokens = Math.max(512, Math.min(configuredOutputTokens, Math.floor(contextWindow * 0.25), contextWindow - 512));
3584
+ const outputReserveTokens = Math.max(MIN_OUTPUT_RESERVE_TOKENS, Math.min(configuredOutputTokens, Math.floor(contextWindow * 0.25), contextWindow - MIN_OUTPUT_RESERVE_TOKENS));
3575
3585
  const availableInputTokens = Math.max(256, contextWindow - outputReserveTokens - 512);
3576
3586
  const conversation = input.conversationId
3577
3587
  ? this.store.getAiConversationContext(input.conversationId, input.workId, input.excludeConversationMessageId)
@@ -3590,6 +3600,7 @@ export class AiManager {
3590
3600
  - functionTokens);
3591
3601
  return {
3592
3602
  contextWindow,
3603
+ configuredOutputTokens,
3593
3604
  outputReserveTokens,
3594
3605
  availableInputTokens,
3595
3606
  conversation,
@@ -3619,7 +3630,10 @@ export class AiManager {
3619
3630
  const threshold = Math.min(90, Math.max(50, Number(this.store.getWorkAiSettings(input.workId).contextCompactThreshold) || 85));
3620
3631
  const conversation = budget.conversation;
3621
3632
  const conversationUsagePercent = Number(budget.conversationUsagePercent) || 0;
3633
+ const configuredOutputTokens = Number(budget.configuredOutputTokens) || DEFAULT_MAX_TOKENS;
3634
+ const maxOutputUsagePercent = Math.min(100, Math.round(configuredOutputTokens / contextWindow * 100));
3622
3635
  const compactableMessageCount = Math.max(0, (conversation?.messages.length ?? 0) - 2);
3636
+ const contextFallbackReached = remainingTokens <= MIN_CONTEXT_REMAINING_TOKENS;
3623
3637
  return {
3624
3638
  modelId: stringValue(model, "id"),
3625
3639
  contextWindow,
@@ -3628,8 +3642,12 @@ export class AiManager {
3628
3642
  conversationTokens: Number(budget.conversationTokens),
3629
3643
  conversationBudgetTokens: Number(budget.conversationBudgetTokens),
3630
3644
  conversationUsagePercent,
3645
+ maxOutputTokens: configuredOutputTokens,
3646
+ maxOutputUsagePercent,
3647
+ maxOutputThresholdReached: maxOutputUsagePercent >= threshold,
3631
3648
  outputReserveTokens: Number(budget.outputReserveTokens),
3632
3649
  remainingTokens,
3650
+ contextFallbackReached,
3633
3651
  usagePercent: Math.min(100, Math.round(inputTokens / contextWindow * 100)),
3634
3652
  tokenDistribution: {
3635
3653
  systemPromptTokens,
@@ -3640,7 +3658,7 @@ export class AiManager {
3640
3658
  },
3641
3659
  compactThreshold: threshold,
3642
3660
  compactableMessageCount,
3643
- compactRecommended: compactableMessageCount > 0 && conversationUsagePercent >= threshold,
3661
+ compactRecommended: compactableMessageCount > 0 && (conversationUsagePercent >= threshold || contextFallbackReached),
3644
3662
  contextWarningPending: conversation?.warningPending ?? false,
3645
3663
  compactedMessageCount: conversation?.compactedMessageCount ?? 0,
3646
3664
  includedContextBlocks: contextPlan.includedBlockIds.length,
@@ -3665,6 +3683,7 @@ export class AiManager {
3665
3683
  contextWindow,
3666
3684
  inputTokens,
3667
3685
  remainingTokens,
3686
+ contextFallbackReached: remainingTokens <= MIN_CONTEXT_REMAINING_TOKENS,
3668
3687
  usagePercent: Math.min(100, Math.round(inputTokens / contextWindow * 100)),
3669
3688
  tokenDistribution: {
3670
3689
  systemPromptTokens,
@@ -3678,8 +3697,11 @@ export class AiManager {
3678
3697
  inspectConversationContext(input) {
3679
3698
  const usage = this.getContextUsage({ ...input, taskType: "chat" });
3680
3699
  const usagePercent = Number(usage.usagePercent) || 0;
3700
+ const maxOutputThresholdReached = usage.maxOutputThresholdReached === true;
3701
+ const contextFallbackReached = usage.contextFallbackReached === true;
3681
3702
  const compactableMessageCount = Number(usage.compactableMessageCount) || 0;
3682
- if (usagePercent >= FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT) {
3703
+ const outputThresholdNeedsCompaction = (maxOutputThresholdReached || contextFallbackReached) && compactableMessageCount > 0;
3704
+ if (usagePercent >= FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT || outputThresholdNeedsCompaction) {
3683
3705
  if (compactableMessageCount <= 0) {
3684
3706
  throw new AppError(409, "AI_CONTEXT_COMPACTION_UNAVAILABLE", "当前请求已占满模型上下文,但没有可压缩的较早对话;请缩短问题、减少引用或新开对话");
3685
3707
  }
@@ -3712,10 +3734,19 @@ export class AiManager {
3712
3734
  }
3713
3735
  const compaction = await this.compactConversation(input);
3714
3736
  if (compaction.changed !== true) {
3737
+ const inputBelowForcedThreshold = (Number(inspection.usage.usagePercent) || 0) < FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT;
3738
+ if ((inspection.usage.maxOutputThresholdReached === true || inspection.usage.contextFallbackReached === true) && inputBelowForcedThreshold) {
3739
+ return {
3740
+ action: "ready",
3741
+ reason: "output_budget_already_fits",
3742
+ usage: { ...inspection.usage, contextWarningPending: false }
3743
+ };
3744
+ }
3715
3745
  throw new AppError(409, "AI_CONTEXT_COMPACTION_UNAVAILABLE", "当前请求已占满模型上下文,但没有可压缩的较早对话;请缩短问题、减少引用或新开对话");
3716
3746
  }
3717
3747
  const compactedUsage = this.getContextUsage({ ...input, taskType: "chat" });
3718
- if ((Number(compactedUsage.usagePercent) || 0) >= FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT) {
3748
+ if ((Number(compactedUsage.usagePercent) || 0) >= FORCE_CONVERSATION_COMPACTION_USAGE_PERCENT
3749
+ || compactedUsage.contextFallbackReached === true) {
3719
3750
  throw new AppError(413, "AI_CONTEXT_STILL_OVER_LIMIT", "自动压缩后当前请求仍占满模型上下文;请缩短问题、减少引用或新开对话");
3720
3751
  }
3721
3752
  return {
@@ -4751,6 +4782,8 @@ export class AiManager {
4751
4782
  ...this.sanitizeParameters({ ...preset, ...(input.parameters ?? {}) }, stringValue(model, "model_id")),
4752
4783
  ...thinkingParameters(provider, model)
4753
4784
  };
4785
+ const configuredOutputTokens = Number(requestedParameters.max_tokens) || DEFAULT_MAX_TOKENS;
4786
+ const contextCompactThreshold = Math.min(90, Math.max(50, Number(this.store.getWorkAiSettings(input.workId).contextCompactThreshold) || 85));
4754
4787
  let effectiveInput = input;
4755
4788
  let context = this.buildContext(effectiveInput, model);
4756
4789
  let messages = this.buildMessages(effectiveInput, context);
@@ -5167,7 +5200,14 @@ export class AiManager {
5167
5200
  // 新工具结果可能附带 toolCallQuotaNotice,预估体积时一并计入,避免低估后触发上下文溢出。
5168
5201
  const noticeBudgetChars = Math.max(agentToolCallQuotaNoticeBudgetChars(1, agentToolCallLimit), agentToolCallQuotaNoticeBudgetChars(agentToolCallSoftWarningThreshold(agentToolCallLimit), agentToolCallLimit));
5169
5202
  const maximumNewToolTokens = Math.ceil((AGENT_TOOL_RESULT_MAX_CHARS + noticeBudgetChars) * 1.1) * Math.max(1, toolCallCount);
5170
- return currentTokens + maximumNewToolTokens + TOOL_CONTEXT_RESPONSE_RESERVE_TOKENS >= contextWindow;
5203
+ // 这里只按工具结果写入后的 context 剩余判断;输出 max_tokens 由下方独立判断。
5204
+ const projectedContextTokens = currentTokens + maximumNewToolTokens;
5205
+ const projectedUsagePercent = Math.round(projectedContextTokens / contextWindow * 100);
5206
+ const projectedContextRemainingTokens = Math.max(0, contextWindow - projectedContextTokens);
5207
+ const maxOutputThresholdReached = configuredOutputTokens >= contextWindow * contextCompactThreshold / 100;
5208
+ return projectedUsagePercent >= contextCompactThreshold
5209
+ || maxOutputThresholdReached
5210
+ || projectedContextRemainingTokens <= MIN_CONTEXT_REMAINING_TOKENS;
5171
5211
  };
5172
5212
  let payload = await requestCompletion("auto");
5173
5213
  let choice = payload.choices?.[0];
@@ -9187,7 +9227,7 @@ export class AiManager {
9187
9227
  if (typeof output.top_p === "number")
9188
9228
  output.top_p = clamp(output.top_p, 0, 1);
9189
9229
  output.max_tokens = typeof output.max_tokens === "number"
9190
- ? Math.round(clamp(output.max_tokens, 1, 32_768))
9230
+ ? Math.round(clamp(output.max_tokens, 1, MAX_MODEL_OUTPUT_TOKENS))
9191
9231
  : DEFAULT_MAX_TOKENS;
9192
9232
  return output;
9193
9233
  }