@jsonstudio/llms 0.6.473 → 0.6.568

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.
Files changed (82) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
  2. package/dist/conversion/codecs/openai-openai-codec.js +2 -1
  3. package/dist/conversion/codecs/responses-openai-codec.js +3 -2
  4. package/dist/conversion/compat/actions/claude-thinking-tools.d.ts +15 -0
  5. package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
  6. package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
  7. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  8. package/dist/conversion/compat/profiles/chat-gemini.json +15 -14
  9. package/dist/conversion/compat/profiles/chat-glm.json +194 -194
  10. package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
  11. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  12. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  13. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  14. package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
  15. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  16. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
  18. package/dist/conversion/hub/pipeline/hub-pipeline.js +40 -13
  19. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
  20. package/dist/conversion/hub/process/chat-process.js +107 -26
  21. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
  22. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +28 -10
  23. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
  24. package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
  25. package/dist/conversion/hub/tool-session-compat.js +299 -0
  26. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  27. package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
  28. package/dist/conversion/responses/responses-openai-bridge.js +0 -71
  29. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  30. package/dist/conversion/shared/args-mapping.js +11 -3
  31. package/dist/conversion/shared/gemini-tool-utils.js +8 -0
  32. package/dist/conversion/shared/responses-output-builder.js +47 -88
  33. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  34. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  35. package/dist/conversion/shared/text-markup-normalizer.js +42 -27
  36. package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
  37. package/dist/conversion/shared/tool-governor.js +75 -4
  38. package/dist/conversion/shared/tool-harvester.js +43 -12
  39. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  40. package/dist/conversion/shared/tool-mapping.js +33 -13
  41. package/dist/filters/index.d.ts +1 -0
  42. package/dist/filters/index.js +1 -0
  43. package/dist/filters/special/request-toolcalls-stringify.js +5 -55
  44. package/dist/filters/special/request-tools-normalize.js +14 -23
  45. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  46. package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
  47. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  48. package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
  49. package/dist/guidance/index.js +70 -27
  50. package/dist/router/virtual-router/bootstrap.js +10 -5
  51. package/dist/router/virtual-router/classifier.js +9 -4
  52. package/dist/router/virtual-router/engine-health.d.ts +22 -0
  53. package/dist/router/virtual-router/engine-health.js +423 -0
  54. package/dist/router/virtual-router/engine-logging.d.ts +20 -0
  55. package/dist/router/virtual-router/engine-logging.js +197 -0
  56. package/dist/router/virtual-router/engine-selection.d.ts +32 -0
  57. package/dist/router/virtual-router/engine-selection.js +649 -0
  58. package/dist/router/virtual-router/engine.d.ts +21 -14
  59. package/dist/router/virtual-router/engine.js +200 -523
  60. package/dist/router/virtual-router/message-utils.js +22 -0
  61. package/dist/router/virtual-router/routing-instructions.d.ts +8 -1
  62. package/dist/router/virtual-router/routing-instructions.js +137 -3
  63. package/dist/router/virtual-router/tool-signals.js +57 -11
  64. package/dist/router/virtual-router/types.d.ts +30 -0
  65. package/dist/router/virtual-router/types.js +1 -1
  66. package/dist/servertool/engine.js +3 -0
  67. package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
  68. package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
  69. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  70. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  71. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
  72. package/dist/servertool/handlers/stop-message-auto.js +204 -0
  73. package/dist/servertool/handlers/vision.js +105 -7
  74. package/dist/servertool/server-side-tools.d.ts +3 -0
  75. package/dist/servertool/server-side-tools.js +29 -0
  76. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
  77. package/dist/tools/apply-patch-structured.d.ts +20 -0
  78. package/dist/tools/apply-patch-structured.js +239 -0
  79. package/dist/tools/tool-description-utils.d.ts +5 -0
  80. package/dist/tools/tool-description-utils.js +50 -0
  81. package/dist/tools/tool-registry.js +14 -5
  82. package/package.json +2 -2
@@ -0,0 +1,204 @@
1
+ import { registerServerToolHandler } from '../registry.js';
2
+ import { cloneJson } from '../server-side-tools.js';
3
+ import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } from '../../router/virtual-router/sticky-session-store.js';
4
+ const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
5
+ function debugLog(message, extra) {
6
+ if (!STOPMESSAGE_DEBUG) {
7
+ return;
8
+ }
9
+ try {
10
+ // eslint-disable-next-line no-console
11
+ console.log(`\x1b[38;5;33m[stopMessage][debug] ${message}` +
12
+ (extra ? ` ${JSON.stringify(extra)}` : '') +
13
+ '\x1b[0m');
14
+ }
15
+ catch {
16
+ /* ignore logging failures */
17
+ }
18
+ }
19
+ const FLOW_ID = 'stop_message_flow';
20
+ const handler = async (ctx) => {
21
+ const record = ctx.adapterContext;
22
+ debugLog('handler_start', {
23
+ requestId: record.requestId,
24
+ providerProtocol: record.providerProtocol
25
+ });
26
+ const followupRaw = record.serverToolFollowup;
27
+ if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
28
+ debugLog('skip_servertool_followup_flag');
29
+ return null;
30
+ }
31
+ const connectionState = resolveClientConnectionState(record.clientConnectionState);
32
+ if (connectionState?.disconnected === true) {
33
+ debugLog('skip_client_disconnected');
34
+ return null;
35
+ }
36
+ const clientDisconnectedRaw = record.clientDisconnected;
37
+ if (clientDisconnectedRaw === true ||
38
+ (typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
39
+ debugLog('skip_client_disconnected_flag');
40
+ return null;
41
+ }
42
+ const stickyKey = resolveStickyKey(record);
43
+ if (!stickyKey) {
44
+ debugLog('skip_no_sticky_key');
45
+ return null;
46
+ }
47
+ const state = loadRoutingInstructionStateSync(stickyKey);
48
+ if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
49
+ debugLog('skip_no_state', { stickyKey });
50
+ return null;
51
+ }
52
+ const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
53
+ const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
54
+ ? Math.max(1, Math.floor(state.stopMessageMaxRepeats))
55
+ : 0;
56
+ if (!text || maxRepeats <= 0) {
57
+ debugLog('skip_invalid_text_or_maxRepeats', {
58
+ stickyKey,
59
+ textLength: text.length,
60
+ maxRepeats
61
+ });
62
+ return null;
63
+ }
64
+ const used = typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
65
+ ? Math.max(0, Math.floor(state.stopMessageUsed))
66
+ : 0;
67
+ if (used >= maxRepeats) {
68
+ debugLog('skip_reached_max_repeats', {
69
+ stickyKey,
70
+ used,
71
+ maxRepeats
72
+ });
73
+ return null;
74
+ }
75
+ if (!isStopFinishReason(ctx.base)) {
76
+ debugLog('skip_not_stop_finish_reason', {
77
+ stickyKey
78
+ });
79
+ return null;
80
+ }
81
+ const captured = getCapturedRequest(ctx.adapterContext);
82
+ if (!captured) {
83
+ debugLog('skip_no_captured_request', {
84
+ stickyKey
85
+ });
86
+ return null;
87
+ }
88
+ state.stopMessageUsed = used + 1;
89
+ state.stopMessageLastUsedAt = Date.now();
90
+ saveRoutingInstructionStateAsync(stickyKey, state);
91
+ const followupPayload = buildStopMessageFollowupPayload(captured, text);
92
+ if (!followupPayload) {
93
+ debugLog('skip_failed_build_followup', {
94
+ stickyKey
95
+ });
96
+ return null;
97
+ }
98
+ return {
99
+ chatResponse: ctx.base,
100
+ execution: {
101
+ flowId: FLOW_ID,
102
+ followup: {
103
+ requestIdSuffix: ':stop_followup',
104
+ payload: followupPayload,
105
+ metadata: {
106
+ serverToolFollowup: true,
107
+ stream: false,
108
+ preserveRouteHint: false,
109
+ disableStickyRoutes: true,
110
+ ...(connectionState ? { clientConnectionState: connectionState } : {})
111
+ }
112
+ }
113
+ }
114
+ };
115
+ };
116
+ registerServerToolHandler('stop_message_auto', handler, { trigger: 'auto' });
117
+ function resolveStickyKey(record) {
118
+ const sessionId = typeof record.sessionId === 'string' && record.sessionId.trim() ? record.sessionId.trim() : '';
119
+ const conversationId = typeof record.conversationId === 'string' && record.conversationId.trim()
120
+ ? record.conversationId.trim()
121
+ : '';
122
+ if (sessionId) {
123
+ return `session:${sessionId}`;
124
+ }
125
+ if (conversationId) {
126
+ return `conversation:${conversationId}`;
127
+ }
128
+ return undefined;
129
+ }
130
+ function isStopFinishReason(base) {
131
+ if (!base || typeof base !== 'object' || Array.isArray(base)) {
132
+ return false;
133
+ }
134
+ const payload = base;
135
+ const choicesRaw = payload.choices;
136
+ if (!Array.isArray(choicesRaw) || !choicesRaw.length) {
137
+ return false;
138
+ }
139
+ const first = choicesRaw[0];
140
+ if (!first || typeof first !== 'object' || Array.isArray(first)) {
141
+ return false;
142
+ }
143
+ const finishReasonRaw = first.finish_reason;
144
+ const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
145
+ ? finishReasonRaw.trim().toLowerCase()
146
+ : '';
147
+ if (finishReason !== 'stop') {
148
+ return false;
149
+ }
150
+ const message = first.message &&
151
+ typeof first.message === 'object' &&
152
+ !Array.isArray(first.message)
153
+ ? first.message
154
+ : null;
155
+ if (!message) {
156
+ return false;
157
+ }
158
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
159
+ if (toolCalls.length > 0) {
160
+ return false;
161
+ }
162
+ return true;
163
+ }
164
+ function getCapturedRequest(adapterContext) {
165
+ if (!adapterContext || typeof adapterContext !== 'object') {
166
+ return null;
167
+ }
168
+ const captured = adapterContext.capturedChatRequest;
169
+ if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
170
+ return null;
171
+ }
172
+ return captured;
173
+ }
174
+ function buildStopMessageFollowupPayload(source, text) {
175
+ if (!source || typeof source !== 'object') {
176
+ return null;
177
+ }
178
+ const payload = {};
179
+ if (typeof source.model === 'string' && source.model.trim()) {
180
+ payload.model = source.model.trim();
181
+ }
182
+ const rawMessages = source.messages;
183
+ const messages = Array.isArray(rawMessages) ? cloneJson(rawMessages) : [];
184
+ messages.push({
185
+ role: 'user',
186
+ content: text
187
+ });
188
+ payload.messages = messages;
189
+ if (Array.isArray(source.tools) && source.tools.length) {
190
+ payload.tools = cloneJson(source.tools);
191
+ }
192
+ const parameters = source.parameters;
193
+ if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
194
+ const params = cloneJson(parameters);
195
+ Object.assign(payload, params);
196
+ }
197
+ return payload;
198
+ }
199
+ function resolveClientConnectionState(value) {
200
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
201
+ return null;
202
+ }
203
+ return value;
204
+ }
@@ -60,7 +60,26 @@ function shouldRunVisionFlow(ctx) {
60
60
  if (followupFlag) {
61
61
  return false;
62
62
  }
63
- return record.hasImageAttachment === true || record.hasImageAttachment === 'true';
63
+ const hasImageAttachment = record.hasImageAttachment === true || record.hasImageAttachment === 'true';
64
+ if (!hasImageAttachment) {
65
+ return false;
66
+ }
67
+ // 若当前已经使用具备内建多模态能力的 Provider(例如 Gemini/Claude/ChatGPT 路径),
68
+ // 且未显式 forceVision,则不再触发额外的 vision 二跳,避免同一轮请求跑两次。
69
+ const forceVision = record.forceVision === true || record.forceVision === 'true';
70
+ if (forceVision) {
71
+ return true;
72
+ }
73
+ const providerType = typeof record.providerType === 'string' ? record.providerType.toLowerCase() : '';
74
+ const providerProtocol = typeof record.providerProtocol === 'string' ? record.providerProtocol.toLowerCase() : '';
75
+ const inlineMultimodal = providerType === 'gemini' ||
76
+ providerType === 'responses' ||
77
+ providerProtocol === 'gemini-chat' ||
78
+ providerProtocol === 'openai-responses';
79
+ if (inlineMultimodal) {
80
+ return false;
81
+ }
82
+ return true;
64
83
  }
65
84
  function getCapturedRequest(adapterContext) {
66
85
  if (!adapterContext || typeof adapterContext !== 'object') {
@@ -80,15 +99,15 @@ function buildVisionAnalysisPayload(source) {
80
99
  if (typeof source.model === 'string' && source.model.trim()) {
81
100
  payload.model = source.model.trim();
82
101
  }
83
- if (Array.isArray(source.messages)) {
84
- payload.messages = cloneJson(source.messages);
85
- }
86
- else {
102
+ const rawMessages = source.messages;
103
+ if (!Array.isArray(rawMessages) || !rawMessages.length) {
87
104
  return null;
88
105
  }
89
- if (Array.isArray(source.tools) && source.tools.length) {
90
- payload.tools = cloneJson(source.tools);
106
+ const visionMessages = buildVisionAnalysisMessages(rawMessages);
107
+ if (!visionMessages.length) {
108
+ return null;
91
109
  }
110
+ payload.messages = visionMessages;
92
111
  const parameters = source.parameters;
93
112
  if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) {
94
113
  const params = cloneJson(parameters);
@@ -96,6 +115,85 @@ function buildVisionAnalysisPayload(source) {
96
115
  }
97
116
  return payload;
98
117
  }
118
+ function buildVisionAnalysisMessages(sourceMessages) {
119
+ const latestUser = extractLatestUserMessageForVision(sourceMessages);
120
+ if (!latestUser) {
121
+ return [];
122
+ }
123
+ const userMessage = buildVisionUserMessage(latestUser);
124
+ if (!userMessage) {
125
+ return [];
126
+ }
127
+ const messages = [];
128
+ const systemMessage = buildVisionSystemMessage();
129
+ if (systemMessage) {
130
+ messages.push(systemMessage);
131
+ }
132
+ messages.push(userMessage);
133
+ return messages;
134
+ }
135
+ function extractLatestUserMessageForVision(sourceMessages) {
136
+ for (let idx = sourceMessages.length - 1; idx >= 0; idx -= 1) {
137
+ const msg = sourceMessages[idx];
138
+ if (!msg || typeof msg !== 'object' || Array.isArray(msg)) {
139
+ continue;
140
+ }
141
+ const role = msg.role;
142
+ if (typeof role === 'string' && role.trim().toLowerCase() === 'user') {
143
+ return cloneJson(msg);
144
+ }
145
+ }
146
+ return null;
147
+ }
148
+ function buildVisionSystemMessage() {
149
+ const content = '你是一名专业的图像分析助手。无论输入是界面截图、文档、图表、代码编辑器、应用窗口还是普通照片,都需要先用结构化、详细的自然语言完整描述画面内容(关键区域、文字信息、布局层次、颜色与对比度、元素之间的关系等),然后总结出与用户任务最相关的关键信息和潜在问题,最后给出具体、可执行的改进建议或结论,避免泛泛而谈。';
150
+ return {
151
+ role: 'system',
152
+ content
153
+ };
154
+ }
155
+ function buildVisionUserMessage(source) {
156
+ const roleRaw = source.role;
157
+ const role = typeof roleRaw === 'string' && roleRaw.trim().length
158
+ ? roleRaw.trim()
159
+ : 'user';
160
+ const rawContent = source.content;
161
+ const message = { role };
162
+ if (Array.isArray(rawContent)) {
163
+ const textParts = [];
164
+ const imageParts = [];
165
+ for (const part of rawContent) {
166
+ if (!part || typeof part !== 'object' || Array.isArray(part)) {
167
+ textParts.push(part);
168
+ continue;
169
+ }
170
+ const record = part;
171
+ const typeValue = typeof record.type === 'string' ? record.type.toLowerCase() : '';
172
+ if (typeValue.includes('image')) {
173
+ imageParts.push(part);
174
+ }
175
+ else {
176
+ textParts.push(part);
177
+ }
178
+ }
179
+ const combined = [];
180
+ if (textParts.length)
181
+ combined.push(...textParts);
182
+ if (imageParts.length)
183
+ combined.push(...imageParts);
184
+ if (!combined.length) {
185
+ return null;
186
+ }
187
+ message.content = combined;
188
+ }
189
+ else if (typeof rawContent === 'string' && rawContent.trim().length) {
190
+ message.content = rawContent.trim();
191
+ }
192
+ else {
193
+ return null;
194
+ }
195
+ return message;
196
+ }
99
197
  function buildVisionFollowupPayload(source, summary) {
100
198
  if (!source || typeof source !== 'object') {
101
199
  return null;
@@ -2,6 +2,9 @@ import type { JsonObject } from '../conversion/hub/types/json.js';
2
2
  import type { ServerSideToolEngineOptions, ServerSideToolEngineResult, ToolCall } from './types.js';
3
3
  import './handlers/web-search.js';
4
4
  import './handlers/vision.js';
5
+ import './handlers/gemini-empty-reply-continue.js';
6
+ import './handlers/iflow-model-error-retry.js';
7
+ import './handlers/stop-message-auto.js';
5
8
  export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;
6
9
  export declare function extractToolCalls(chatResponse: JsonObject): ToolCall[];
7
10
  export declare function cloneJson<T>(value: T): T;
@@ -1,11 +1,17 @@
1
1
  import { getServerToolHandler, listAutoServerToolHandlers } from './registry.js';
2
2
  import './handlers/web-search.js';
3
3
  import './handlers/vision.js';
4
+ import './handlers/gemini-empty-reply-continue.js';
5
+ import './handlers/iflow-model-error-retry.js';
6
+ import './handlers/stop-message-auto.js';
4
7
  export async function runServerSideToolEngine(options) {
5
8
  const base = asObject(options.chatResponse);
6
9
  if (!base) {
7
10
  return { mode: 'passthrough', finalChatResponse: options.chatResponse };
8
11
  }
12
+ if (isClientDisconnected(options.adapterContext)) {
13
+ return { mode: 'passthrough', finalChatResponse: base };
14
+ }
9
15
  const toolCalls = extractToolCalls(base);
10
16
  const contextBase = {
11
17
  base,
@@ -89,6 +95,29 @@ function getArray(value) {
89
95
  export function cloneJson(value) {
90
96
  return JSON.parse(JSON.stringify(value));
91
97
  }
98
+ function isClientDisconnected(adapterContext) {
99
+ if (!adapterContext || typeof adapterContext !== 'object') {
100
+ return false;
101
+ }
102
+ const state = adapterContext.clientConnectionState;
103
+ if (state && typeof state === 'object' && !Array.isArray(state)) {
104
+ const disconnected = state.disconnected;
105
+ if (disconnected === true) {
106
+ return true;
107
+ }
108
+ if (typeof disconnected === 'string' && disconnected.trim().toLowerCase() === 'true') {
109
+ return true;
110
+ }
111
+ }
112
+ const raw = adapterContext.clientDisconnected;
113
+ if (raw === true) {
114
+ return true;
115
+ }
116
+ if (typeof raw === 'string' && raw.trim().toLowerCase() === 'true') {
117
+ return true;
118
+ }
119
+ return false;
120
+ }
92
121
  export function extractTextFromChatLike(payload) {
93
122
  let current = payload;
94
123
  const visited = new Set();
@@ -132,6 +132,22 @@ export function createAnthropicResponseBuilder(options) {
132
132
  },
133
133
  getResult() {
134
134
  if (!state.completed) {
135
+ // 对部分实现(或网络提前关闭)导致缺失 message_stop 的 SSE 流,
136
+ // 只要已经累计到可用内容,就以最佳努力方式返回结果,而不是直接抛错。
137
+ if (state.content.length > 0) {
138
+ return {
139
+ success: true,
140
+ response: {
141
+ id: state.id || `msg_${Date.now()}`,
142
+ type: 'message',
143
+ role: state.role || 'assistant',
144
+ model: state.model || 'unknown',
145
+ content: state.content,
146
+ usage: state.usage,
147
+ stop_reason: state.stopReason ?? 'end_turn'
148
+ }
149
+ };
150
+ }
135
151
  return { success: false, error: new Error('Anthropic SSE stream incomplete') };
136
152
  }
137
153
  return {
@@ -0,0 +1,20 @@
1
+ export type StructuredApplyPatchKind = 'insert_after' | 'insert_before' | 'replace' | 'delete' | 'create_file' | 'delete_file';
2
+ export interface StructuredApplyPatchChange {
3
+ file?: string;
4
+ kind: StructuredApplyPatchKind | string;
5
+ anchor?: string;
6
+ target?: string;
7
+ lines?: string[] | string;
8
+ use_anchor_indent?: boolean;
9
+ }
10
+ export interface StructuredApplyPatchPayload extends Record<string, unknown> {
11
+ instructions?: string;
12
+ file?: string;
13
+ changes: StructuredApplyPatchChange[];
14
+ }
15
+ export declare class StructuredApplyPatchError extends Error {
16
+ reason: string;
17
+ constructor(reason: string, message: string);
18
+ }
19
+ export declare function buildStructuredPatch(payload: StructuredApplyPatchPayload): string;
20
+ export declare function isStructuredApplyPatchPayload(candidate: unknown): candidate is StructuredApplyPatchPayload;
@@ -0,0 +1,239 @@
1
+ export class StructuredApplyPatchError extends Error {
2
+ reason;
3
+ constructor(reason, message) {
4
+ super(message);
5
+ this.reason = reason;
6
+ }
7
+ }
8
+ const SUPPORTED_KINDS = [
9
+ 'insert_after',
10
+ 'insert_before',
11
+ 'replace',
12
+ 'delete',
13
+ 'create_file',
14
+ 'delete_file'
15
+ ];
16
+ const FILE_PATH_INVALID_RE = /[\r\n]/;
17
+ const INVALID_FILE_FORMAT_RE = /^([A-Za-z]:|\/)/;
18
+ const toSafeString = (value, label) => {
19
+ const str = typeof value === 'string' ? value : '';
20
+ if (!str.trim()) {
21
+ throw new StructuredApplyPatchError('missing_field', `${label} is required`);
22
+ }
23
+ return str;
24
+ };
25
+ const normalizeFilePath = (raw, label) => {
26
+ const trimmed = raw.trim();
27
+ if (!trimmed) {
28
+ throw new StructuredApplyPatchError('invalid_file', `${label} must not be empty`);
29
+ }
30
+ if (FILE_PATH_INVALID_RE.test(trimmed)) {
31
+ throw new StructuredApplyPatchError('invalid_file', `${label} must be a single-line relative path`);
32
+ }
33
+ if (INVALID_FILE_FORMAT_RE.test(trimmed)) {
34
+ throw new StructuredApplyPatchError('invalid_file', `${label} must be relative to the workspace root`);
35
+ }
36
+ return trimmed.replace(/\\/g, '/');
37
+ };
38
+ const splitTextIntoLines = (input) => {
39
+ const normalized = input.replace(/\r/g, '');
40
+ const parts = normalized.split('\n');
41
+ if (parts.length && parts[parts.length - 1] === '') {
42
+ parts.pop();
43
+ }
44
+ return parts.length ? parts : [''];
45
+ };
46
+ const normalizeLines = (value, label) => {
47
+ if (Array.isArray(value)) {
48
+ if (!value.length) {
49
+ return [];
50
+ }
51
+ return value.map((entry, idx) => {
52
+ if (typeof entry !== 'string') {
53
+ if (entry === null || entry === undefined) {
54
+ return '';
55
+ }
56
+ return String(entry);
57
+ }
58
+ // Preserve intentional whitespace
59
+ return entry.replace(/\r/g, '');
60
+ });
61
+ }
62
+ if (typeof value === 'string') {
63
+ return splitTextIntoLines(value);
64
+ }
65
+ if (value === null || value === undefined) {
66
+ throw new StructuredApplyPatchError('invalid_lines', `${label} must be an array of strings or a multi-line string`);
67
+ }
68
+ return [String(value)];
69
+ };
70
+ const buildContextLines = (raw) => splitTextIntoLines(raw).map((line) => ` ${line}`);
71
+ const buildPrefixedLines = (lines, prefix) => lines.map((line) => `${prefix}${line}`);
72
+ const detectIndentFromAnchor = (anchorLines, mode) => {
73
+ const source = mode === 'first' ? anchorLines[0] ?? '' : anchorLines[anchorLines.length - 1] ?? '';
74
+ const match = source.match(/^(\s*)/);
75
+ return match ? match[1] ?? '' : '';
76
+ };
77
+ const applyAnchorIndent = (lines, anchorLines, position, enabled) => {
78
+ if (!enabled) {
79
+ return lines;
80
+ }
81
+ const indent = detectIndentFromAnchor(anchorLines, position);
82
+ if (!indent) {
83
+ return lines;
84
+ }
85
+ return lines.map((line) => {
86
+ if (!line.trim()) {
87
+ return line;
88
+ }
89
+ if (/^\s/.test(line)) {
90
+ return line;
91
+ }
92
+ return `${indent}${line}`;
93
+ });
94
+ };
95
+ export function buildStructuredPatch(payload) {
96
+ if (!payload || typeof payload !== 'object') {
97
+ throw new StructuredApplyPatchError('missing_payload', 'apply_patch arguments must be a JSON object');
98
+ }
99
+ if (!Array.isArray(payload.changes) || payload.changes.length === 0) {
100
+ throw new StructuredApplyPatchError('missing_changes', 'apply_patch requires a non-empty "changes" array');
101
+ }
102
+ const topLevelFile = typeof payload.file === 'string' && payload.file.trim()
103
+ ? normalizeFilePath(payload.file, 'file')
104
+ : undefined;
105
+ const sectionOrder = [];
106
+ const fileSections = new Map();
107
+ const ensureUpdateSection = (file) => {
108
+ const existing = fileSections.get(file);
109
+ if (existing) {
110
+ if (existing.type !== 'update') {
111
+ throw new StructuredApplyPatchError('invalid_change_sequence', `File "${file}" already marked as ${existing.type}`);
112
+ }
113
+ return existing;
114
+ }
115
+ const created = { type: 'update', hunks: [] };
116
+ sectionOrder.push(file);
117
+ fileSections.set(file, created);
118
+ return created;
119
+ };
120
+ for (const [index, change] of payload.changes.entries()) {
121
+ if (!change || typeof change !== 'object') {
122
+ throw new StructuredApplyPatchError('invalid_change', `Change at index ${index} must be an object`);
123
+ }
124
+ const kindRaw = typeof change.kind === 'string' ? change.kind.trim().toLowerCase() : '';
125
+ if (!kindRaw) {
126
+ throw new StructuredApplyPatchError('invalid_change_kind', `Change at index ${index} is missing "kind"`);
127
+ }
128
+ if (!SUPPORTED_KINDS.includes(kindRaw)) {
129
+ throw new StructuredApplyPatchError('invalid_change_kind', `Unsupported change kind "${change.kind}" at index ${index}`);
130
+ }
131
+ const file = change.file ? normalizeFilePath(change.file, `changes[${index}].file`) : topLevelFile;
132
+ if (!file) {
133
+ throw new StructuredApplyPatchError('invalid_file', `Change at index ${index} is missing "file"`);
134
+ }
135
+ if (kindRaw === 'create_file') {
136
+ if (fileSections.has(file)) {
137
+ throw new StructuredApplyPatchError('invalid_change_sequence', `File "${file}" already has pending changes`);
138
+ }
139
+ const lines = normalizeLines(change.lines, `changes[${index}].lines`);
140
+ sectionOrder.push(file);
141
+ fileSections.set(file, { type: 'add', lines });
142
+ continue;
143
+ }
144
+ if (kindRaw === 'delete_file') {
145
+ if (fileSections.has(file)) {
146
+ throw new StructuredApplyPatchError('invalid_change_sequence', `File "${file}" already has pending changes`);
147
+ }
148
+ sectionOrder.push(file);
149
+ fileSections.set(file, { type: 'delete' });
150
+ continue;
151
+ }
152
+ const section = ensureUpdateSection(file);
153
+ switch (kindRaw) {
154
+ case 'insert_after': {
155
+ const anchor = toSafeString(change.anchor, `changes[${index}].anchor`);
156
+ const anchorLines = splitTextIntoLines(anchor);
157
+ const additions = normalizeLines(change.lines, `changes[${index}].lines`);
158
+ if (!additions.length) {
159
+ throw new StructuredApplyPatchError('invalid_lines', `changes[${index}].lines must include at least one line`);
160
+ }
161
+ const prepared = applyAnchorIndent(additions, anchorLines, 'last', change.use_anchor_indent);
162
+ const hunkBody = [...buildContextLines(anchor), ...buildPrefixedLines(prepared, '+')];
163
+ section.hunks.push(hunkBody);
164
+ break;
165
+ }
166
+ case 'insert_before': {
167
+ const anchor = toSafeString(change.anchor, `changes[${index}].anchor`);
168
+ const anchorLines = splitTextIntoLines(anchor);
169
+ const additions = normalizeLines(change.lines, `changes[${index}].lines`);
170
+ if (!additions.length) {
171
+ throw new StructuredApplyPatchError('invalid_lines', `changes[${index}].lines must include at least one line`);
172
+ }
173
+ const prepared = applyAnchorIndent(additions, anchorLines, 'first', change.use_anchor_indent);
174
+ const hunkBody = [...buildPrefixedLines(prepared, '+'), ...buildContextLines(anchor)];
175
+ section.hunks.push(hunkBody);
176
+ break;
177
+ }
178
+ case 'replace': {
179
+ const target = toSafeString(change.target, `changes[${index}].target`);
180
+ const replacements = normalizeLines(change.lines, `changes[${index}].lines`);
181
+ const hunkBody = [
182
+ ...buildPrefixedLines(splitTextIntoLines(target), '-'),
183
+ ...buildPrefixedLines(replacements, '+')
184
+ ];
185
+ section.hunks.push(hunkBody);
186
+ break;
187
+ }
188
+ case 'delete': {
189
+ const target = toSafeString(change.target, `changes[${index}].target`);
190
+ const hunkBody = buildPrefixedLines(splitTextIntoLines(target), '-');
191
+ section.hunks.push(hunkBody);
192
+ break;
193
+ }
194
+ default: {
195
+ throw new StructuredApplyPatchError('invalid_change_kind', `Unsupported change kind "${change.kind}" at index ${index}`);
196
+ }
197
+ }
198
+ }
199
+ if (!sectionOrder.length) {
200
+ throw new StructuredApplyPatchError('missing_changes', 'apply_patch payload produced no file operations');
201
+ }
202
+ const lines = ['*** Begin Patch'];
203
+ for (const file of sectionOrder) {
204
+ const section = fileSections.get(file);
205
+ if (!section)
206
+ continue;
207
+ if (section.type === 'add') {
208
+ lines.push(`*** Add File: ${file}`);
209
+ for (const line of section.lines) {
210
+ lines.push(`+${line}`);
211
+ }
212
+ }
213
+ else if (section.type === 'delete') {
214
+ lines.push(`*** Delete File: ${file}`);
215
+ }
216
+ else {
217
+ lines.push(`*** Update File: ${file}`);
218
+ for (const hunk of section.hunks) {
219
+ lines.push('@@');
220
+ for (const entry of hunk) {
221
+ lines.push(entry);
222
+ }
223
+ lines.push('@@');
224
+ }
225
+ }
226
+ }
227
+ lines.push('*** End Patch');
228
+ return lines.join('\n');
229
+ }
230
+ export function isStructuredApplyPatchPayload(candidate) {
231
+ if (!candidate || typeof candidate !== 'object') {
232
+ return false;
233
+ }
234
+ const record = candidate;
235
+ if (!Array.isArray(record.changes)) {
236
+ return false;
237
+ }
238
+ return true;
239
+ }
@@ -0,0 +1,5 @@
1
+ export declare function normalizeToolName(value: unknown): string;
2
+ export declare function isShellToolName(value: unknown): boolean;
3
+ export declare function hasApplyPatchToolDeclared(tools: unknown[] | undefined): boolean;
4
+ export declare function buildShellDescription(toolDisplayName: string, hasApplyPatch: boolean): string;
5
+ export declare function appendApplyPatchReminder(description: string, hasApplyPatch: boolean): string;