File size: 4,054 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
 * v2 import-context construction — pure functions that replicate, byte for
 * byte, the user message the legacy v1 engine appended for an `importContext`
 * RPC, plus its validation and overflow rejection.
 *
 * The v2 engine has no import-context capability of its own (nothing under
 * `agent-core-v2` builds this message), but all of its primitives — the same
 * wire `context.append_message` Op, the same token estimator, the same model
 * capabilities — are available, so the SDK composes the legacy behavior on top
 * of them. Keep the wrapper format, the guidance text, and the two XML
 * escapers byte-identical with the legacy v1 output so a v1-written and a
 * v2-written import reduce to the same history.
 */
import type { ContextMessage } from '@moonshot-ai/agent-core-v2';
import { estimateTokensForMessages } from '@moonshot-ai/agent-core-v2/llm-adapter/contract/tokens';

import { ErrorCodes, KimiError } from '#/errors';

/** Byte-identical with v1's `IMPORT_CONTEXT_GUIDANCE`. */
const IMPORT_CONTEXT_GUIDANCE =
  'This is a prior conversation history that may be relevant to the current session. ' +
  'Please review this context and use it to inform your responses.';

/** Byte-identical with v1's `escapeXml` (& < > "). */
function escapeXml(input: string): string {
  return input
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;');
}

/** Byte-identical with v1's `escapeXmlAttr` (& " only). */
function escapeXmlAttr(input: string): string {
  return input.replaceAll('&', '&amp;').replaceAll('"', '&quot;');
}

/**
 * The exact message v1 appends for an import, including its rejections:
 * blank content (`import_content_empty`) and blank source
 * (`import_source_empty`) fail with v1's `request.invalid` shapes before any
 * token math runs.
 */
export function buildImportContextMessage(content: string, source: string): ContextMessage {
  if (content.trim().length === 0) {
    throw new KimiError(ErrorCodes.REQUEST_INVALID, 'Imported context cannot be empty', {
      details: { reason: 'import_content_empty' },
    });
  }
  const normalizedSource = source.trim();
  if (normalizedSource.length === 0) {
    throw new KimiError(ErrorCodes.REQUEST_INVALID, 'Imported context source cannot be empty', {
      details: { reason: 'import_source_empty' },
    });
  }
  return {
    role: 'user',
    content: [
      {
        type: 'text',
        text:
          `<system>The user has imported context from ${escapeXml(normalizedSource)}. ` +
          `${IMPORT_CONTEXT_GUIDANCE}</system>`,
      },
      {
        type: 'text',
        text:
          `<imported_context source="${escapeXmlAttr(normalizedSource)}">\n` +
          `${content}\n</imported_context>`,
      },
    ],
    toolCalls: [],
    origin: { kind: 'user' },
  };
}

/**
 * v1's overflow gate: the import estimate plus the current context must fit
 * the model window (unknown window = `0` skips the check on both engines).
 * The estimator is the same character heuristic on both sides, so the counts
 * — and therefore the rejection — agree.
 */
export function assertImportFits(
  message: ContextMessage,
  currentTokenCount: number,
  maxContextTokens: number,
): void {
  const importTokenCount = estimateTokensForMessages([message]);
  const totalTokenCount = currentTokenCount + importTokenCount;
  if (maxContextTokens > 0 && totalTokenCount > maxContextTokens) {
    throw new KimiError(
      ErrorCodes.CONTEXT_OVERFLOW,
      'Imported content is too large for the current model context ' +
        `(~${String(importTokenCount)} import tokens + ~${String(currentTokenCount)} existing ` +
        `= ~${String(totalTokenCount)} total > ${String(maxContextTokens)} token limit). ` +
        'Please import a smaller file or session.',
      {
        details: {
          reason: 'import_context_overflow',
          importTokenCount,
          currentTokenCount,
          totalTokenCount,
          maxContextTokens,
        },
      },
    );
  }
}